How to Translate an HTML File Without Breaking Tags, Attributes, or Links
What to translate in an HTML file, what to leave alone, and how to check it. With a sample page, a free checker, and a 9-run local LLM test.

To translate an HTML file, translate the text people read and the attributes that carry reader-facing words, such as alt, title, placeholder, aria-label, and the page's meta description. Leave the markup alone: tag names, href, src, id, class, scripts, styles, and anything marked translate="no". Then change one value that is easy to forget, the lang attribute on the <html> element, so browsers, screen readers, and search engines know which language the page is now in.
Keeping the tags intact turned out to be the easy part. We translated a 59-line HTML help page into Simplified Chinese nine times with a local model, using three methods, and checked every output with a script. Every run kept every tag, link, and ID. What went wrong was quieter: the model translated a line inside a <pre> code block in all six runs that sent the whole file, it left the navigation's aria-label in English in all six, and the method that keeps the markup safest by design, translating text pieces one by one, produced a sentence that told the reader to do the wrong thing 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? |
|---|---|---|
| Visible text | headings, paragraphs, table cells, link text, <label> | Yes |
<title> and meta description | <title>Export Your Library | Shelfmark Help</title> | Yes. Both show up in search results and browser tabs. |
og:title and other social meta tags | <meta property="og:title" content="Export Your Library"> | Yes, the content value only |
alt, title, placeholder, aria-label | alt="The Export dialog with EPUB selected", aria-label="Breadcrumb" | Yes |
lang on <html> | <html lang="en"> | Change it to the target language, for example zh-Hans |
| Tag and attribute names | <a>, <img>, width, aria-label itself | No |
href, src, id, class, for, name, type, data-* | href="#choose-format", id="q" | No |
<code>, <pre>, <script>, <style> | shelfmark export --format epub | No. Decide separately about code comments and sample output. |
| Text set by CSS | .note::before { content: "Note: "; } | Yes, but it lives in the stylesheet, so plan for it |
| JSON-LD structured data | "name": "Export your Shelfmark library" | Translate the name and description values; keep keys and the JSON valid |
Elements with translate="no" | <span translate="no">Shelfmark</span> | No |
| HTML comments | <!-- Generated by the help-center build... --> | Optional. Readers never see them. |
| Character references | <library>, , © | Keep the escaped ones escaped. < must not become a real <. |
This list is not our invention. The HTML Standard's section on the translate attribute defines "translatable attributes": title on every element, alt on images, placeholder on inputs, content on meta elements whose name is known to be translatable, and lang, which it says must be changed to match the new language. It also names ARIA's aria-label as an example of an attribute other specifications can add, and it says that in a translate="no" element, text and attribute values "are to be left as-is when the page is localized."
What We Tested
We wrote sample-source.html, a help-center page for the same fictional reading app, Shelfmark, that we used in our Markdown translation test. It has one example of each row in the table above, plus a few traps:
- a sentence split by inline tags: "Before you export, back up your notes. An export copies your books, but it does not delete anything from Shelfmark," with the product name inside
translate="no"; - a sentence with two inline
<code>elements whose order matters: "Type the path to the<library>folder after--out"; - a
<pre><code>block with a command and one line of sample output,# Export finished: 128 books; - a CSS rule that inserts the word "Note:" before a paragraph;
- an in-page link to
#choose-format, amailto:link, a search form, and a JSON-LD block.
Then we asked a local model to translate it into Simplified Chinese in three ways:
- Whole file. Paste the whole file and ask for the translated file back.
- Instructed. The same, plus eight rules: keep every tag and attribute name, do not change
href,src,id,classand similar values, translate the listed text attributes and meta tags, leave<code>,<pre>,<script>, and<style>alone except the JSON-LDnameanddescription, respecttranslate="no", keep character references and comments, and setlangtozh-Hans. - Segments. The approach of tools that extract text from HTML: a script splits the file into tags and text, skips
<code>,<pre>,<script>,<style>, andtranslate="no"elements, sends the remaining text pieces and text attributes to the model as a numbered JSON list, and writes the translations back into the untouched markup. It also setslang="zh-Hans"itself.
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 method three times, nine runs in total, on October 27, 2026. The script is run_llm_html_test.py, and all nine outputs are published unedited; the README lists every file. If you have not set up a local model before, 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 tools.
Result: The Markup Survived; Three Decisions Did Not
We checked every output with check_html_translation.py, a checker that uses only Python's standard library. The full output is in check-results.txt, and results.csv has one row per run.
| Check | Whole file (3 runs) | Instructed (3 runs) | Segments (3 runs) |
|---|---|---|---|
Same 58 tags in order; every href, src, id, class unchanged | 3 of 3 | 3 of 3 | 3 of 3 |
translate="no" text kept, in-page link still works, <library> still escaped | 3 of 3 | 3 of 3 | 3 of 3 |
Sample output line inside <pre> translated | 3 of 3 | 3 of 3 | 0 of 3 (skipped by design) |
CSS content: "Note: " translated | 3 of 3 | 0 of 3 | 0 of 3 (skipped by design) |
JSON-LD name and description translated | 3 of 3 | 3 of 3 | 0 of 3 (skipped by design) |
aria-label="Breadcrumb" translated | 0 of 3 | 0 of 3 | 3 of 3 |
Sentence with two inline <code> elements says the right thing | 3 of 3 | 3 of 3 | 0 of 3 |
| Spaces between Chinese text across an inline tag | none | 0, 5, and 4 | 6 in every run |
lang on <html> | zh-CN (not asked) | zh-Hans | zh-Hans (set by the script) |
Every one of the nine runs kept the same 58 tags in the same order, every href, src, id, and class, the in-page link target, the comment, the escaped <library>, and the product name inside translate="no". If you were worried that a model would mangle the markup of a simple page, it did not. The problems were all in what to translate.
1. Sample output inside <pre> got translated
The code block in our sample ends with a line of program output:
<pre><code>shelfmark export --format epub --covers --out ~/Books/export
# Export finished: 128 books</code></pre>
In all six whole-file and instructed runs, the model returned # 导出完成:128 本书 on that line, including the three runs where the prompt said not to change anything inside <pre>. The command itself was never touched. The model treated the line as a comment, and translating comments is a reasonable choice in a tutorial. But this line is what the program prints, and a reader who compares it with their terminal will see English. Decide before you translate whether sample output and code comments are translated, and tell the tool, or protect <pre> blocks completely.
The whole-file runs also translated the CSS content: "Note: " in the <style> block, while the instructed runs, told to leave <style> alone, kept it in English. Neither is wrong on its own. Translating the stylesheet is correct for a page where the CSS is inline, and wrong for a site where one shared stylesheet serves every language. Either way, it is a decision someone has to make, not a detail a model should guess.
2. Accessibility labels were skipped
The breadcrumb navigation is <nav aria-label="Breadcrumb">. A screen reader announces that label; sighted readers never see it. In all six whole-file and instructed runs, it stayed in English, even though the instructed prompt listed aria-label among the attributes to translate. In one instructed run, the search button's aria-label="Search" stayed in English too.
The segment method translated it every time, because its script collected every aria-label, alt, title, and placeholder value and asked for each one explicitly. That is the strongest argument for extracting text attributes on purpose rather than hoping the model notices them.
3. Translating pieces kept the tags and broke the sentence
The segment method is safe for the markup by construction: the model never sees a tag, so it cannot damage one. It passed every structural check in all three runs. It also produced the only translation in the test that gave the reader wrong instructions.
The English sentence is "Type the path to the <library> folder after --out." Inline <code> splits it into three pieces: "Type the path to the", "folder after", and a period. Chinese puts these words in a different order, so a translator needs to move the two code elements. The segment method cannot move them. All three runs reversed the instruction. Run 2 produced this (runs 1 and 3 say 后 instead of 之后, with the same meaning):
在 <code><library></code> 之后输入 <code>--out</code>文件夹的路径。
That reads "after <library>, type the path of the --out folder," the reverse of the instruction. All six whole-file and instructed runs moved the code elements and got it right:
在 <code>--out</code> 后面输入 <code><library></code> 文件夹的路径。
The same splitting left stray spaces where a tag meets Chinese text, such as 前往 <b>设置</b> and <b>书库</b> ,, in all three segment runs, six places per run. A browser shows them as visible gaps, including before Chinese punctuation. The whole-file runs had none; two of the three instructed runs had four or five.
A structural checker cannot catch the reversed sentence, because every tag is exactly where it was. Our checker reports when inline code changes order, which is normal for many languages, but that warning cannot tell right from wrong. In this test it fired on all six correct translations and stayed silent on the three wrong ones, because the wrong ones kept the English order. Only a person reading the sentence can tell. The fix is to send whole sentences, with their inline tags, to the translator, which is what the whole-file runs did, and to check structure afterwards.
Smaller differences
lang: the whole-file runs changed it tozh-CNon their own. The instructed runs usedzh-Hans, as asked. Both are valid language tags;zh-Hansnames the script (Simplified Chinese),zh-CNnames a region. The W3C's guidance on language declarations recommends declaring the language on the<html>element and not with aContent-Languagemeta tag.- JSON-LD: the whole-file and instructed runs translated the
nameanddescriptionvalues and kept the JSON valid. Our segment script skipped<script>content, so its structured data stayed in English, which is how a tool that ignores scripts will behave too. - Character references: the whole-file and instructed runs kept
›, ,©, and the emoji reference as written. Five of those six runs rewrote the range2–5in Chinese words (2 到 5 or 2 至 5); one instructed run wrote a plain dash instead of the reference, although its prompt said to keep references. Our segment script decoded all of them and wrote characters back, which displays the same. The one that matters,<library>inside<code>, stayed escaped in all nine runs.
Before You Translate: Prepare the File
- Mark what must not change. Put
translate="no"on product names, model numbers, code-like strings in prose, and anything else that must stay as it is. It is part of HTML, it costs nothing, and the model respected it in all six runs that sent it the whole file. Segment tools that read it, like our script, skip those elements entirely. Our do-not-translate guide covers how to decide what goes on that list. - Decide the code policy. Are code comments translated? Is sample output? Is the stylesheet shared across languages? Write the answer down, together with the names that must not change, in a translation glossary, and give it to whoever or whatever translates.
- Keep IDs in the source language. In-page links point at
idvalues. If someone translates anid, every link to it breaks. None of our runs did, but a translator who "helpfully" localizes anchors will. - Collect the hidden text. List the attributes that hold words,
alt,title,placeholder,aria-label,valueon buttons, meta tags, and JSON-LD, so they are checked and not forgotten. - Do not translate JavaScript strings in place. Messages set by a script, such as an alert or a button label created in code, need a separate localization step in the code. None of the six whole-file or instructed runs changed our script block, which is correct, but it means those strings stay in English.
After You Translate: Check It
-
Run the structural check.
python3 check_html_translation.py source.html translated.html --lang zh-Hans --keep ShelfmarkIt compares tags and their order, every attribute that is not reader-facing text, the contents of
<pre>,<script>, and<style>, inline code, JSON-LD keys,translate="no"text, comments, and in-page links. It warns about text attributes still in English, alangvalue that differs from what you expect, visible text that looks untranslated, inline code that changed order, and spaces between Chinese, Japanese, or Korean text across an inline tag. It does not judge translation quality. -
Read every sentence that contains an inline tag. This is where the meaning broke in our test. Do not rely on the checker's "inline code reordered" warning to find these sentences: in our test, the wrong translations were the ones that kept the original order.
-
Open the page in a browser. Click the in-page links, tab through the form with a screen reader if you can, and look for gaps around bold and linked text.
-
For right-to-left languages, also set
dir="rtl"on the<html>element. Our RTL book translation guide covers what else changes. -
For XHTML, such as the chapter files inside an EPUB, also confirm that the file is still well-formed XML, for example with
xmllint --noout chapter.xhtml. A browser forgives an unclosed tag; an e-reader may not. Our EPUB translation QA checklist covers the rest of an ebook.
Where BookTranslator Fits
BookTranslator's upload page accepts .html, .htm, and .xhtml files alongside books and documents, so you can upload a saved page or a static help article as a whole file. We did not run this sample through BookTranslator for this article, and we have not tested how it handles inline code order, CSS content, or JSON-LD, so check its output the same way you would check any other tool's. You see the price before you pay.
Whatever you use, do the preparation steps before translating and the checks afterwards. A page with every tag intact can still tell a reader to type the wrong thing. If your HTML is really documentation written in Markdown, our Markdown translation guide covers the source format instead.
Get the Test Files
Everything from this test is free to reuse:
- sample-source.html: the 59-line English sample;
- check_html_translation.py: the structural checker (Python standard library only);
- run_llm_html_test.py: the three methods, for any OpenAI-compatible local server;
- the nine outputs in the outputs folder, with check-results.txt and results.csv.
Related Posts





