Skip to content

번역 문서 포맷 깨짐 원인 및 해결안

This content is not available in your language yet.

번역 문서 포맷 깨짐 원인 및 해결안

섹션 제목: “번역 문서 포맷 깨짐 원인 및 해결안”

번역된 영어 문서(en)가 한국어 원본(ko)과 비교했을 때 마크다운 포맷이 깨져 렌더링됩니다.

문서KO 원본 (예시)EN 번역본 (문제)
seeds-api각 문단·리스트 항목이 줄 단위로 구분, ---·## 전후 빈 줄 유지본문이 한두 줄로 합쳐짐, 리스트가 한 줄에 이어짐
data-management-api동일동일
infrastructure-api동일동일
category-reference테이블·헤딩이 줄 단위 유지테이블 행·헤딩이 한 줄에 합쳐짐
output-json-scheme코드 블록·헤딩·테이블 구조 유지`## Pipeline Stages ```` 처럼 헤딩과 코드 블록이 한 줄에 붙음

KO (seeds-api.md) — 정상:

엔진 핵심 인터페이스: ...
---
## Health
시스템 상태 확인용 ...
- **GET /health** — 기본 헬스체크.
- **GET /health/ready** — 레디니스.
- **GET /health/live** — 라이브니스.
루트 **GET /** ...

EN (seeds-api.md) — 깨짐:

Engine Core Interfaces: ... --- ## Health System status verification ...
- **GET /health** — Basic health check. - **GET /health/ready** — Readiness. - **GET /health/live** — Liveness. The root **GET /** ...

즉, 단일 줄바꿈(\n)과 빈 줄(\n\n)이 번역 결과에서 사라지거나 한 줄로 합쳐지는 형태입니다.


  • scripts/translate_docs.py: KO 마크다운을 읽어 본문(body)만 추출 → 코드 블록 치환 → 본문 전체(또는 섹션 단위)를 DeepL에 한 번에 전달 → 치환 복원·후처리(---]---) → EN 파일로 저장.
  • scripts/translate/engine.py: 긴 텍스트는 \n\n+ 기준으로 청크 분할 후 각 청크 번역, 결과를 "".join(translated)로 이어붙임.

2. 근본 원인: DeepL API의 줄바꿈 정규화

섹션 제목: “2. 근본 원인: DeepL API의 줄바꿈 정규화”
  • DeepL이 번역 시 단일 줄바꿈·공백을 정규화하는 것으로 보임. preserve_formatting=True를 켜도, 실제 동작에서는:
    • 문단 내 단일 \n(리스트 항목 구분, --- 전후, ## 전후)이 무시되거나
    • 여러 줄이 한 줄로 합쳐지는 현상이 발생.
  • 따라서 입력의 “줄 구조”가 출력에서 유지되지 않음. 마크다운은 줄 단위 문법이라 이렇게 되면 포맷이 붕괴됨.
  • Full 모드: 본문 전체를 한 번에(또는 \n\n+ 청크만 나눠서) DeepL에 보냄. DeepL이 줄을 합치면 그대로 EN 파일에 기록됨.
  • Incremental 모드: ## 기준으로 섹션을 나누어 섹션별로 번역하지만, 섹션 내부의 줄바꿈은 여전히 DeepL에 맡기므로 섹션 안에서 같은 현상 발생.
  • 후처리: ---]--- 만 하고, 줄바꿈 복구나 구조 복원 로직은 없음.

DeepL API XML 태그 처리 전략 (공식 권장)

섹션 제목: “DeepL API XML 태그 처리 전략 (공식 권장)”

DeepL API를 직접 사용할 때, 번역하지 말아야 할 부분을 보호하고 구조를 유지하려면 XML 태그 처리를 사용하는 것이 권장됩니다.

  • tag_handling: API 호출 시 tag_handling="xml"(또는 tag_handling=xml)을 설정합니다. API는 태그 바깥의 자연어만 골라 번역합니다.
  • ignore_tags: <ignore>와 같은 커스텀 XML 태그로 감싼 영역은 번역·변형되지 않습니다. ignore_tags=["ignore"](또는 ignore_tags=ignore)로 전달합니다.
  • non_splitting_tags: 특정 서식이 포함된 문장이 쪼개지지 않도록 보호할 때 사용합니다. 문장 분할을 막고 싶은 태그를 지정합니다.

번역 전에 다음 요소를 <ignore>...</ignore>(또는 <keep>...</keep> 등)로 감싸서 전송합니다.

대상방법
YAML 프론트매터문서 최상단 ------ 블록 전체를 <ignore>로 감싼 뒤 전송. 또는 정규식으로 분리해 텍스트 값(title, description 등)만 번역하고, 나머지는 번역 대상에서 제외한 뒤 나중에 합치는 방식 권장.
코드 블록``` … ``` 전체를 <ignore>로 감싸 코드가 번역·이동되지 않게 합니다.
인라인 코드`…`<ignore>(또는 <keep>)로 감싸 API가 내용을 번역하지 않도록 합니다.
  • API 응답에서 <ignore>(또는 사용한 태그)를 제거하고, 원래 마크다운 형태만 남깁니다. (태그 제거 시 replace("<ignore>", "").replace("</ignore>", "") 등.)
  • YAML 프론트매터: title, date 등 설정값은 정규식으로 따로 분리해 값만 번역하거나, 아예 번역 대상에서 제외한 뒤 나중에 합치는 방식을 권장합니다.
  • 문장 부호·리스트: 마크다운 리스트 기호(-, *) 뒤에 공백이 없으면 DeepL이 문장 일부로 오인할 수 있으므로, 전처리에서 공백 유무를 점검하는 것이 좋습니다.
  • 복잡한 구조: 이미지 ![alt](url)에서 alt만 번역하려면 별도 정규식이 필요합니다. 하이퍼링크 텍스트만 번역하는 경우도 동일합니다.

XML 전략과 “줄바꿈 붕괴”와의 관계

섹션 제목: “XML 전략과 “줄바꿈 붕괴”와의 관계”
  • ignore_tags로 코드 블록·프론트매터·인라인 코드를 보호하면, 해당 부분은 변형되지 않습니다.
  • 다만 줄바꿈 붕괴번역되는 자연어 구간에서 발생합니다. DeepL이 태그 바깥 텍스트를 번역할 때 단일 줄바꿈·빈 줄을 정규화해 합쳐버리므로, XML 태그만으로는 줄 구조 복원이 보장되지 않습니다.
  • 따라서 XML 태그 처리는 “번역 제외 영역 보호”의 필수 수단으로 두고, 줄 구조 유지는 아래 방안 A(줄바꿈 마스킹) 또는 non_splitting_tags 실험과 조합하는 것을 권장합니다.

방안 E: DeepL XML 태그 처리 (번역 제외 영역 보호) — 필수 적용

섹션 제목: “방안 E: DeepL XML 태그 처리 (번역 제외 영역 보호) — 필수 적용”
  • 원리: API 호출 시 tag_handling="xml", ignore_tags=["ignore"] 사용. 전처리에서 YAML 프론트매터, 코드 블록 ``` … ```, 인라인 코드 `…`<ignore>로 감싼 뒤 전송하고, 응답에서 태그만 제거해 복원.
  • 장점: DeepL 공식 권장 방식. 코드·설정값이 번역되거나 깨지는 것을 원천적으로 막을 수 있음.
  • 단점: 줄바꿈 붕괴(자연어 구간의 줄 합침)는 그대로일 수 있음. 구조 유지를 위해 non_splitting_tags 실험이나 방안 A와 병행 필요.
  • 구현 포인트:
    • 전처리: 정규식으로 ---\n.*?\n---, ```.*?```, `[^`]+` 를 찾아 <ignore>…</ignore> 로 감싼 문자열을 만든 뒤 DeepL에 전달.
    • 후처리: result.text에서 <ignore>, </ignore> 제거.
    • (선택) YAML은 파싱 후 title/description 등 값만 번역하고, 나머지는 그대로 두고 합치는 방식으로 분리 처리.

방안 A: 줄바꿈 마스킹 (구조 유지용 권장)

섹션 제목: “방안 A: 줄바꿈 마스킹 (구조 유지용 권장)”
  • 원리: DeepL에 넘기기 전에 단일 줄바꿈을 고유 플레이스홀더(예: ___NL___)로 치환, 번역 후 결과에서 플레이스홀더를 \n으로 복원.
  • 장점: 구현이 단순하고, 기존 “코드 블록 치환/복원” 패턴과 동일하게 확장 가능. DeepL이 줄을 합쳐도 복원 시 구조가 살아남음.
  • 단점: 플레이스홀더가 번역되거나 깨지지 않도록 토큰을 신중히 선택해야 함(이미 사용 중인 ___MD_BLOCK_·___GLOSSARY_와 구분, 문장 중간에 나올 가능성 낮은 문자열).
  • 구현 포인트:
    • 본문에서 코드 블록(또는 <ignore> 처리된 영역) 추출 후, 남은 본문에서 \n → 플레이스홀더 치환 (빈 줄 \n\n은 별도 플레이스홀더 시퀀스로 유지).
    • DeepL 호출 후 플레이스홀더 → \n 복원.
    • 방안 E와 병행 시: <ignore> 감싸기 → 그 바깥 텍스트에만 줄바꿈 마스킹 적용 후 API 호출 → 복원 순서로 처리.

방안 B: 문단/블록 단위 번역 후 재조합

섹션 제목: “방안 B: 문단/블록 단위 번역 후 재조합”
  • 원리: 본문을 “빈 줄로 구분된 블록” 단위로 쪼개서, 각 블록만 DeepL에 넘기고, 결과를 원본 순서대로 원래 줄바꿈 개수에 맞춰 이어붙임.
  • 장점: 블록 사이의 빈 줄은 우리가 강제로 넣을 수 있음.
  • 단점: 블록 내부 줄바꿈은 여전히 DeepL에 의존. 블록이 너무 작으면 번역 품질 하락.

방안 C: 번역 후 마크다운 구조 후처리 (휴리스틱)

섹션 제목: “방안 C: 번역 후 마크다운 구조 후처리 (휴리스틱)”
  • 원리: 번역 결과 문자열에서 --- , ## , - ** 등 패턴을 찾아, 그 앞에 줄바꿈을 삽입하는 규칙을 적용.
  • 장점: API/전처리는 건드리지 않아도 됨.
  • 단점: 예외 케이스 많음. 유지보수 부담과 오동작 위험.
  • 원리: mdast 등으로 파싱 후 “텍스트 노드”만 추출해 번역하고, 나머지 구조는 그대로 유지.
  • 장점: 포맷 붕괴를 원천적으로 막을 수 있음.
  • 단점: 구현 비용이 큼.

  1. 필수: 방안 E(DeepL XML 태그 처리) 적용

    • tag_handling="xml", ignore_tags=["ignore"] 사용.
    • 전처리: YAML 프론트매터, 코드 블록 ``` … ```, 인라인 코드 `…`<ignore>로 감싼 뒤 전송.
    • 후처리: 응답에서 <ignore>, </ignore> 제거.
    • YAML은 값만 번역하거나 번역 대상에서 제외 후 합치는 방식 권장. 리스트 기호(-, *) 뒤 공백 점검.
  2. 구조 유지: 방안 A(줄바꿈 마스킹) 또는 non_splitting_tags 실험

    • XML로 보호한 뒤에도 자연어 구간에서 줄이 합쳐지면, 본문 내 \n → 플레이스홀더 치환·복원(방안 A)을 적용.
    • 또는 DeepL의 non_splitting_tags로 구조 단위(예: 커스텀 태그로 감싼 “한 줄”)가 쪼개지지 않게 하는 방법을 시험.
  3. 검증: v1 API 문서 5개(seeds-api, data-management-api, infrastructure-api, category-reference, output-json-scheme)에 대해 KO→EN 번역을 다시 수행한 뒤, EN 마크다운이 KO와 동일한 줄/단락 구조를 갖는지 확인.

  4. 기존 유지: ---]--- 후처리는 그대로 유지.

  5. 소스 언어 고정: source_lang="KO" 명시

    • 자동 감지 시 영어로 오인해 원문 그대로 반환(identical to source)하는 문제 방지.
    • API 호출 시 항상 KO → EN-US로 고정.
  6. XML 유효성: <ignore> 바깥 텍스트에서 &, <, > 이스케이프

    • 예: “Research & Seed” → “Research & Seed” (전송 전).
    • 응답 후 &amp;, &lt;, &gt; 복원.
    • “Tag handling parsing failed, invalid token” 방지.

다음이 코드에 반영되어 있습니다.

항목구현 위치
방안 E (XML 태그 처리)translate_docs.py: _wrap_body_for_xml() — 코드 블록·인라인 코드를 <ignore>로 감싼 뒤 전송. tag_handling="xml", ignore_tags=["ignore"] 전달.
방안 A (줄바꿈 마스킹)translate_docs.py: _mask_newlines() / _unmask_newlines()\n___NL___ 치환·복원.
XML 이스케이프translate_docs.py: _escape_xml_outside_ignore(), _unescape_xml_text()<ignore> 밖의 &, <, > 이스케이프 및 응답 후 복원.
source_lang=KOengine.py: translate_one/translate_textsource_lang 인자. translate_docs.py: SOURCE_LANG_KO = "KO", 모든 번역 호출에 전달.
후처리_strip_ignore_tags(), _unescape_xml_text(), _postprocess_translated_body() (---]---).

  • translate-horizontal-rule-artifact---] 아티팩트 원인 및 후처리.
  • 번역 스크립트: scripts/translate_docs.py, scripts/translate/engine.py, scripts/translate/placeholders.py.
  • DeepL API: tag_handling, ignore_tags, non_splitting_tagsDeepL API 문서 (XML Handling 가이드).