|
22 | 22 | DISALLOWED_SPECIAL = "<|endoftext|>" |
23 | 23 | REPLACEMENT_TOKEN = "<END_OF_TEXT>" |
24 | 24 |
|
| 25 | +# mdBook directives are executable document structure, not prose. Models can |
| 26 | +# occasionally translate directive names or attributes (for example |
| 27 | +# `name` -> `naam`), which makes preprocessors abort a whole language build. |
| 28 | +MDBOOK_DIRECTIVE_RE = re.compile(r"\{\{#[^{}]*\}\}") |
| 29 | +MDBOOK_TAB_OPEN_RE = re.compile(r"\{\{#tab\b([^{}]*)\}\}") |
| 30 | + |
25 | 31 | TOKENIZER_FALLBACKS = [ |
26 | 32 | ("gpt-5", "o200k_base"), |
27 | 33 | ("gpt-4o", "o200k_base"), |
@@ -124,9 +130,37 @@ def _get_encoding_for_model(model: str): |
124 | 130 | print(f"Tokenizer for model {model} not found. Falling back to {FINAL_TOKENIZER_FALLBACK}.") |
125 | 131 | return tiktoken.get_encoding(FINAL_TOKENIZER_FALLBACK) |
126 | 132 |
|
127 | | -def _fix_translated_shortcodes(text: str) -> str: |
128 | | - """Keep mdBook shortcode attribute names valid after translation.""" |
129 | | - return re.sub(r'(\{\{#tab\s+)(?!name=)[^=\s}]+=', r'\1name=', text) |
| 133 | +def preserve_mdbook_directives(source: str, translated: str) -> str: |
| 134 | + """Restore mdBook directives exactly as they appeared in the source. |
| 135 | +
|
| 136 | + Prompt instructions are not an integrity boundary: a translated directive |
| 137 | + can still look plausible while being invalid to a preprocessor. When the |
| 138 | + model preserves the number/order of directives, restore each one |
| 139 | + byte-for-byte. If it adds or removes one, keep the source chunk instead of |
| 140 | + committing structurally broken Markdown to a language branch. |
| 141 | + """ |
| 142 | + source_directives = MDBOOK_DIRECTIVE_RE.findall(source) |
| 143 | + if not source_directives: |
| 144 | + return translated |
| 145 | + |
| 146 | + translated_directives = MDBOOK_DIRECTIVE_RE.findall(translated) |
| 147 | + if len(source_directives) != len(translated_directives): |
| 148 | + print( |
| 149 | + "Directive count changed during translation " |
| 150 | + f"({len(source_directives)} -> {len(translated_directives)}); " |
| 151 | + "returning the source chunk unchanged." |
| 152 | + ) |
| 153 | + return source |
| 154 | + |
| 155 | + directives = iter(source_directives) |
| 156 | + restored = MDBOOK_DIRECTIVE_RE.sub(lambda _match: next(directives), translated) |
| 157 | + |
| 158 | + for match in MDBOOK_TAB_OPEN_RE.finditer(restored): |
| 159 | + if not re.search(r'\bname\s*=\s*(["\']).*?\1', match.group(1)): |
| 160 | + print("Invalid mdBook tab directive after translation; returning the source chunk unchanged.") |
| 161 | + return source |
| 162 | + |
| 163 | + return restored |
130 | 164 |
|
131 | 165 | def reportTokens(prompt, model): |
132 | 166 | encoding = _get_encoding_for_model(model) |
@@ -306,7 +340,10 @@ def translate_text(language, text, file_path, model, cont=0, slpitted=False, cli |
306 | 340 | response_message = response_message.replace("bypassy", "bypasses") # PL translations translates that from time to time |
307 | 341 | response_message = response_message.replace("Bypassy", "Bypasses") |
308 | 342 | response_message = response_message.replace("-privec.md", "-privesc.md") # PL translations translates that from time to time |
309 | | - response_message = _fix_translated_shortcodes(response_message) |
| 343 | + |
| 344 | + # Restore source directives after all model-provided text normalization. |
| 345 | + # This preserves exact mdBook syntax while retaining translated prose. |
| 346 | + response_message = preserve_mdbook_directives(text, response_message) |
310 | 347 |
|
311 | 348 | # Sometimes chatgpt modified the number of "#" at the beginning of the text, so we need to fix that. This is specially important for the first line of the MD that mucst have only 1 "#" |
312 | 349 | cont2 = 0 |
|
0 commit comments