Skip to content

feat(translation-pipeline): LLM translation pipeline with deterministic preservation gate - #568

Open
MaxLee-dev wants to merge 93 commits into
mainfrom
deepl-translation-layer
Open

feat(translation-pipeline): LLM translation pipeline with deterministic preservation gate#568
MaxLee-dev wants to merge 93 commits into
mainfrom
deepl-translation-layer

Conversation

@MaxLee-dev

@MaxLee-dev MaxLee-dev commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

개요

컴포넌트 JSDoc을 영어 정본으로 두고, 문서 사이트에 노출되는 한국어는 파이프라인 산출물로만 만든다. 이 PR은 그 파이프라인(scripts/translation-pipeline)과 216개 컴포넌트의 번역 결과, 그리고 사이트 연동까지를 담는다.

JSDoc을 한국어로 쓰지 않는 이유는 IDE 호버다. 호버가 읽는 .d.ts에는 파이프라인이 손을 못 댄다. 한국어를 정본으로 삼으면 base-ui 상속 props와 한국어 설명이 영구히 섞이고, 영어를 정본으로 두면 모든 표면이 균질해진다.

파이프라인

번역(claude-sonnet-4-6)
  → 결정론 문자열 보존 체크 + MQM 평가(gemini-3-pro)
  → 실패 시 후편집(claude-sonnet-4-6) → 재검사
  → 보존 위반이 남으면 영어 원문 유지

평가 모델은 번역과 다른 계열로 갈랐다. 같은 모델이 자기 번역을 채점하면 후하게 준다.

문자열 보존은 LLM에서 떼어냈다

문자열 단위 LLM 판정은 근거가 약하다. MQM Council은 250단어 하한을 두고, WMT25는 세그먼트 레벨에서 LLM이 학습형 메트릭에 밀린다고 보고했다. 한국어 단어 단위 오류 탐지율은 Claude 3.5 기준 0%였다.

그래서 "틀리면 개발자가 잘못 구현하는" 종류의 오류는 validation/preserve.ts의 결정론 체크로 옮겼다.

규칙 검사 내용
backtick_span 백틱 인라인 코드가 원문 그대로 남아 있는지
identifier 백틱 밖 PascalCase·camelCase 식별자가 번역되지 않았는지
url URL이 변형되지 않았는지
markdown_structure 줄머리 마커 순서와 코드펜스 개수가 같은지

위반이 나오면 후편집으로 한 번 복구를 시도하고, 그래도 남으면 한국어를 버리고 영어 원문을 쓴다. 잘못된 식별자가 박힌 한국어보다 영어가 낫다.

MQM 루브릭은 16축에서 6축(Accuracy 3 + Fluency 3)으로 줄었다. 걷어낸 10축은 Terminology·Markup & Code 계열이 대부분인데, 전부 위 결정론 체크가 대신한다.

실행 구조

병목은 순차 루프가 아니라 중복 번역이었다. 번역 유닛 1,167건 중 고유 원문은 300건뿐이다. className 206회, render 206회, style 144회 — base-ui 상속 props가 215개 컴포넌트에 그대로 복사돼 있고, 상위 3건이 전체의 48%를 차지한다.

  • 배치를 만들기 전에 원문으로 중복을 제거하고, 고유 원문만 번역해 결과를 원 유닛 전체에 되뿌린다. 캐시가 원래 이 일을 하지만 동시 실행 16개가 서로를 못 보기 때문에 사전 pass가 필요하다
  • 컴포넌트별 인터리브를 단계 분리(번역 전수 → MQM 전수)로 바꿨다. 컴포넌트 이름은 번역에만 값어치가 있어서(MQM은 원문↔번역문 대조다) 유닛별 필드로 내리고 배치는 컴포넌트를 횡단한다
  • 배치 단위 동시성 16. 워커 풀은 손으로 만들었다 — 이 패키지의 런타임 의존성은 meow 하나뿐이다
  • 배치 id는 getTranslationUnitKey()(${componentIndex}:${id}). 기존 props[0].size.description은 컴포넌트를 섞으면 충돌하고, reconcileById가 조용히 엉뚱한 유닛에 결과를 매핑한다
  • MQM 배치는 10에서 74로 올렸다. 근거는 (타임아웃 − 여유) × 실측 처리량 ÷ 유닛당 출력 토큰 ÷ 안전계수 2이고, 실측 81 tok/s 기준으로 번역 20은 그대로 맞다
  • 429·5xx·타임아웃은 지수 백오프로 2회 재시도(1s→4s). 4xx는 재시도하지 않는다

456콜·수 시간·$58이던 전량 실행이 **20콜·3분·$12**로 내려갔다.

저장 레이아웃과 사이트 연동

  • flat JSON 200개를 지우고 generated/en/(추출 산출물) + generated/ko/(번역 산출물)을 병치했다
  • 소비자 2곳(get-component-doc.ts, component-props-table.tsx)은 ko/를 읽고 없으면 en/으로 폴백한다. 영어 폴백이 여기 얹힌다
  • .translation-cache.json은 커밋하고 .i18n-report.md는 gitignore 대상이다

전량 실행 결과

항목
컴포넌트 216
번역 유닛 1,167 (고유 300)
MQM PASS율 100%
영어 폴백 0건
LLM 콜 20
소요 시간 3분 0초

산출물 전체에 결정론 보존 체크를 다시 돌려 독립 검증했다. 위반 0건, 원문과 동일한 항목 0건, 한글이 없는 항목 0건이다.

검토 포인트

  • preserve.ts의 식별자 규칙은 험프가 2개 이상인 것만 식별자로 본다. Button처럼 험프 하나인 컴포넌트명은 평범한 영어 단어(Whether, This)와 구별할 수 없어 일부러 흘려보냈다
  • translate.ts의 Style 규칙과 validator.tsFluency/Unnatural phrasing은 의도적 미러링 관계다. 한쪽만 고치면 1차 MQM 실패가 늘어 후편집 비용이 커진다
  • 캐시 키에는 루브릭과 프롬프트가 들어가지 않는다. 키는 버전·원문·대상 로케일·모델 3개다. 모델을 바꾸면 전량 미스가 된다

테스트 62개 통과(파이프라인 패키지), lint·typecheck 통과.

뒤늦게 붙인 경계 바깥 수정 (2026-08-03)

파이프라인 내부는 건강했지만 경계 바깥에서 조용히 실패하는 것들이 있었다. 커밋 3개를 얹었다.

추출기가 컴포넌트를 버리고 있었다

findExportedInterfaceProps는 이름과 달리 type alias만 본다. export interface Props로 쓰인 SheetRoot·SheetResizeHandle이 경고 한 줄 없이 사라져 sheet.mdx의 표 두 개가 에러를 렌더했다. 저장소 전체에서 그렇게 쓴 namespace는 이 둘뿐이라 추출기를 넓히는 대신 소스 스타일을 통일했다. 같은 함정을 다음 사람이 또 밟지 않도록 추출기가 버리는 순간 경고를 찍는다.

여기서 고친 것은 소스까지다. sheet-root·sheet-resize-handle 표가 실제로 사이트에 뜨는 것은 재추출 결과를 커밋하는 후속 브랜치가 들어온 뒤다. 이번 커밋에는 재실행으로 생긴 생성 JSON이 들어 있지 않다 — 이 PR이 이미 432개를 안고 있어서 218개를 더 얹으면 리뷰가 불가능해진다. 검증용으로만 돌리고 git checkout으로 원복했다.

MDX가 남의 표를 렌더하고 있었다

절 제목을 kebab-case로 바꿔 componentName과 대조하니 7건이 어긋났다. 깨진 참조는 에러가 보여 언젠가 잡히지만 오매핑은 그럴싸한 남의 표를 조용히 보여준다 — 사용자가 없는 API를 믿는다.

  • floating-bar.mdx 3건은 -primitive 접미사가 빠져 있었다. 그중 popup은 FloatingBar.Popup 표를 두 번 렌더하던 중이었다
  • menu.mdx 3건은 componentName이 아니라 절 제목이 틀렸다. 하위 #### 절에 세 Primitive가 이미 올바르게 있어서, 상위 ### 제목이 각각 Menu.Popup·Menu.Item·Menu.Separator여야 한다. 반대로 고쳤다면 표가 중복되고 공개 파트 셋의 문서가 사라진다
  • toast.mdx### useToastManager 절은 지웠다. 부르던 toast-object는 namespace가 아닌 내부 type이라 추출기가 원리적으로 만들 수 없다. 지금 렌더되는 것은 에러 문자열이라 정보 손실이 없다. 이 절을 어떻게 채울지는 별도 티켓으로 뺐다

나머지

  • pnpm --filter website i18n이 새 클론에서 깨졌다. 두 CLI의 bindist/를 가리키는데 gitignore 대상이다. i18n이 두 패키지를 먼저 빌드한다
  • ko JSON도 prettier를 지난다. en만 지나서 짧은 배열의 인라인 여부가 갈렸다
  • 파이프라인에서 소비처 0건인 코드를 걷어냈다 — client.ts의 토큰·비용 계산, isMqmError, MqmResult.unavailable, CliError. chunkArray·reconcileById는 세 파일에 복사돼 있어서 util.ts 하나로 모았다

확인한 것

재추출·재번역을 한 번 돌려 캐시가 300 hit, 2 miss로 끝나는 것을 봤다. 미스 2건은 새로 잡힌 step·disabled 설명이다. sheet·floating-bar·menu·toast 네 페이지를 로컬에서 열어 표 여섯 개가 제 짝을 렌더하는 것과 콘솔 에러 0건을 확인했다. 대조 스크립트도 다시 돌려 어긋남 0건을 봤다.

한 가지는 감수한다. step@default 16defaultValue: "RESIZE_STEP"으로 나온다. 추출기가 JSDoc 태그가 아니라 구현부의 파라미터 기본값 식별자를 읽기 때문이다. 값이 사라진 것은 아니고 상수 이름으로 표시될 뿐이라 이번에는 두었다.

Summary by CodeRabbit

  • 새로운 기능

    • 웹사이트 문서에 한국어 콘텐츠를 우선 제공하고, 없을 경우 영어 콘텐츠로 자동 대체합니다.
    • 문서 JSON을 번역하고 품질을 검증하는 번역 CLI를 추가했습니다.
    • 번역 결과 캐시와 품질 검증 리포트를 지원합니다.
  • 개선 사항

    • 컴포넌트 및 속성 문서화 규칙을 정비했습니다.
    • 문서 추출 및 Props 처리의 오류 대응을 개선했습니다.
    • 번역 시 코드, URL, 마크다운 구조 등의 보존 검사를 강화했습니다.
  • 문서

    • 번역 설정, 사용 방법, 품질 검증 및 개발 명령을 문서화했습니다.

MaxLee-dev and others added 9 commits April 21, 2026 15:14
Implements the core i18n translation layer:
- deepl.ts: DeepL API client with glossary support and graceful fallback
- llm-client.ts: LiteLLM adapter for Sonnet/Opus LLM calls
- llm-postprocess.ts: LLM post-processing with MQM error feedback loop
- mqm-validator.ts: Binary PASS/FAIL MQM quality validator via LiteLLM
- pipeline.ts: Orchestrates DeepL → LLM → MQM per component with cache support
- cache.ts: Content-addressable translation cache keyed by text+locale+model+glossary
- report.ts: MQM report builder with markdown output (buildReport/renderReport/writeReport)
- types.ts: Shared MqmError, MqmResult, and TranslationConfig types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d CLI

- schema.ts: adds TranslationConfig interface with llm/mqm nested config
- defaults.ts: adds translation defaults (disabled by default)
- models/output.ts: adds locale field to PropsOutput for en/ko locale routing
- cli/index.ts: adds --translate flag (enables translation) and --skip-cache flag

Translation is opt-in. Run with --translate to enable DeepL+MQM pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extract.ts: made async; when translation.enabled, writes en/ subfolder
  then calls translatePropsInfo, builds MQM report, writes ko/ output
- stages/write.ts: buildWriteFiles accepts optional locale param;
  when set, output paths become outputDir/{locale}/*.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sts total)

Adds 43+11=54 new test cases covering:
- cache.test.ts: makeCacheKey, loadCache, saveCache (12 tests)
- deepl.test.ts: no-key, success, error, glossary (5 tests)
- llm-postprocess.test.ts: all 7 paths including MQM error feedback
- mqm-validator.test.ts: all 10 paths including malformed JSON
- pipeline.test.ts: empty entries, cache hit/miss, MQM PASS/FAIL, failOnError (9 tests)
- report.test.ts: buildReport, renderReport, writeReport including error path (11 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ncies

- @anthropic-ai/sdk: for LiteLLM-compatible Sonnet/Opus calls in MQM pipeline
- deepl-node or fetch-based: DeepL translation API client
- p-limit: concurrency limiter (LLM_CONCURRENCY=5) for parallel translation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents DEEPL_API_KEY, LITELLM_BASE_URL, LITELLM_MODEL, and
DEEPL_GLOSSARY_ID environment variables needed for the i18n pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntegration

Documents how the JSDoc extraction pipeline now supports --translate flag
for automatic DeepL + MQM translation to Korean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cache

- mqm-validator.ts: validate LLM JSON output shape before casting to MqmResult;
  malformed responses (missing verdict/errors fields) safely return PASS
- cache.ts: filter cache entries missing the translated field on load;
  prevents undefined propagation if cache file is from an older schema version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l edge cases

pipeline.ts:
- Skip LLM post-processing when DeepL falls back to source text (prevents
  sending English to an LLM asked to edit Korean, which produces garbage)
- Clear MQM errors after successful retry so .i18n-report.md reflects the
  actual final translation, not the pre-retry state
- Don't cache fallback entries (source == translation) to prevent DeepL
  rate-limit failures from permanently poisoning the cache with English text
- failCount now counts individual MQM errors per component, not 0/1 boolean
  (fixes misleading pass rate when a component has multiple failures)

mqm-validator.ts:
- passResult() factory function instead of shared PASS_RESULT singleton to
  prevent accidental mutation of the shared errors array

report.ts:
- Escape bracket characters in e.message to prevent markdown link injection
  from LLM-generated error messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MaxLee-dev
MaxLee-dev requested a review from noahchoii as a code owner April 21, 2026 06:25
@vercel

vercel Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vapor-ui Error Error Aug 3, 2026 11:46pm

Request Review

@changeset-bot

changeset-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9a8d185

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@vapor-ui/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

한국어 번역 CLI와 LiteLLM 연동, MQM 검증, 보존 검사, 캐시와 보고서를 추가했습니다. 웹사이트는 한국어 문서를 우선 로드합니다. JSDoc 규칙, Sheet props 선언과 컴포넌트 문서 항목도 갱신했습니다.

Changes

번역 파이프라인 및 웹사이트 통합

Layer / File(s) Summary
패키지와 실행 환경 구성
pnpm-workspace.yaml, scripts/translation-pipeline/*, apps/website/.env.example, apps/website/package.json
번역 패키지, 워크스페이스, 빌드·테스트 설정, 실행 문서와 환경 설정을 추가했습니다.
번역 계약과 LLM 클라이언트
scripts/translation-pipeline/src/types.ts, src/defaults.ts, src/util.ts, src/translation/*
번역 단위와 결과 계약을 정의했습니다. LiteLLM 요청, JSON Schema, 재시도와 응답 검증을 구현했습니다.
번역과 결정론적 검증
scripts/translation-pipeline/src/translation/*, src/validation/*
한국어 번역을 구현했습니다. 식별자, 코드, URL, Markdown 구조와 MQM 평가 규칙을 검증합니다.
MQM 배치 수명주기와 후편집
scripts/translation-pipeline/src/translator/batch-lifecycle.ts, src/translator/translator.test.ts
초기 평가, 후편집과 최종 평가를 수행합니다. 보존 위반과 배치 오류는 영어 원문으로 폴백합니다.
캐시, 보고서와 번역 오케스트레이션
scripts/translation-pipeline/src/cache/*, src/report/*, src/translator/translator.ts
중복 원문을 제거하고 캐시를 조회합니다. 검증된 결과를 props와 보고서에 반영합니다.
CLI 실행과 웹사이트 로딩
scripts/translation-pipeline/src/cli/*, scripts/translation-pipeline/tests/cli.test.ts, apps/website/src/...
CLI가 ko/ 파일과 품질 보고서를 생성합니다. 웹사이트는 한국어 파일을 먼저 요청하고 영어 파일로 폴백합니다.

컴포넌트 JSDoc 규칙

Layer / File(s) Summary
컴포넌트 요약과 prop 문서화 규칙
.claude/skills/write-component-jsdocs/*
컴포넌트 역할별 요약과 HTML 렌더링 요소 규칙을 추가했습니다. prop과 이벤트 핸들러 예시를 갱신했습니다.

API와 컴포넌트 문서 정렬

Layer / File(s) Summary
추출기와 공개 타입 정렬
scripts/ts-api-extractor/src/*, packages/core/src/components/sheet/sheet.tsx, .changeset/lazy-sheets-align.md
Props type alias 추출 조건과 기본 경로 설명을 변경했습니다. Sheet props를 type alias로 선언했습니다.
컴포넌트 문서 항목 정렬
apps/website/content/docs/components/(components)/*
FloatingBar와 Menu 문서의 Props Table 참조와 컴포넌트 항목을 갱신했습니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 LLM 번역 파이프라인과 결정론적 보존 검사를 명확하게 요약하며 PR의 주요 변경 사항과 일치합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch deepl-translation-layer
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deepl-translation-layer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.claude/skills/write-component-jsdocs/references/guide.md (1)

427-468: ⚠️ Potential issue | 🟠 Major

Update the Complete Example to match the new patterns.

The Complete Example section hasn't been updated to follow the new "Wording patterns by component role" and "Wording patterns by prop category" introduced earlier in this PR:

  1. Line 463 — The component summary uses multiple sentences: "Button component for user interactions. Use for primary actions such as form submission, dialog triggers, and navigation. Renders a <button> element." but the new pattern (line 206) requires one single line. The new top-level component pattern (line 164) specifies: "A [noun phrase] for/that [purpose]." ending with the rendered element.

  2. Line 457 — The event handler description reads "Called when the button is clicked..." but the new event handler pattern (line 301) requires: "Event handler called when [exact condition]."

These inconsistencies will confuse developers who read the Complete Example expecting it to demonstrate best practices.

📝 Proposed updates to align with new patterns
 /**
- * Button component for user interactions. Use for primary actions such as form submission, dialog triggers, and navigation. Renders a `<button>` element.
+ * A button for triggering actions and submitting forms. Renders a `<button>` element.
  */
 export function Button({ label, variant = 'fill', size = 'md', ...props }: ButtonProps) {
     /**
-     * Called when the button is clicked or activated via Enter or Space key.
+     * Event handler called when the button is clicked or activated via Enter or Space key.
      */
     onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/write-component-jsdocs/references/guide.md around lines 427 -
468, Update the JSDoc in the Complete Example to follow the new single-line
component and event wording patterns: change the Button component summary to a
single sentence following "A [noun phrase] for/that [purpose]." and end with the
rendered element (e.g., "A button for primary user actions that renders a
<button> element."), and change the onClick prop JSDoc in ButtonProps to the
event handler pattern "Event handler called when [exact condition]." (e.g.,
"Event handler called when the button is clicked or activated via Enter or Space
key."). Ensure these changes are applied to the Button (component summary) and
onClick (prop) entries.
scripts/ts-api-extractor/src/config/schema.ts (1)

37-96: ⚠️ Potential issue | 🟠 Major

Validate or deep-merge the new translation config before use.

translation is now accepted on ExtractorConfig, but validatePartialConfig never checks its nested shape and mergeConfig replaces it shallowly. A runtime config like { translation: { enabled: true } } can pass validation, overwrite defaults, and later crash when code reads config.translation.llm.enabled or config.translation.validation.mqm.enabled.

🛡️ Proposed hardening direction
+function mergeTranslationConfig(
+    base: ExtractorConfig['translation'],
+    patch: PartialExtractorConfig['translation'],
+): ExtractorConfig['translation'] {
+    if (patch === undefined) return base;
+    if (base === undefined) return patch;
+
+    return {
+        ...base,
+        ...patch,
+        llm: {
+            ...base.llm,
+            ...patch.llm,
+        },
+        validation: {
+            ...base.validation,
+            ...patch.validation,
+            mqm: {
+                ...base.validation.mqm,
+                ...patch.validation?.mqm,
+            },
+        },
+    };
+}
+
 export function mergeConfig(base: ExtractorConfig, patch: PartialExtractorConfig): ExtractorConfig {
     return {
         ...base,
         ...patch,
         exclude: patch.exclude ?? base.exclude,
         includeHtml: patch.includeHtml ?? base.includeHtml,
+        translation: mergeTranslationConfig(base.translation, patch.translation),
         components: {
             ...base.components,
             ...(patch.components ?? {}),
         },
     };
 }

Also add runtime validation for translation.enabled, translation.targetLocale, translation.llm.enabled, and translation.validation.mqm.{enabled,failOnError} when translation is present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/src/config/schema.ts` around lines 37 - 96,
validatePartialConfig currently skips any checks for config.translation and
mergeConfig shallowly replaces translation, allowing invalid or partial shapes
to slip through; add runtime validation inside validatePartialConfig for the
translation object and its nested keys (check translation.enabled boolean,
translation.targetLocale string if present, translation.llm.enabled boolean if
llm present, and translation.validation.mqm.enabled and
translation.validation.mqm.failOnError booleans when validation/mqm are present)
and make mergeConfig perform a deep-merge for the translation key (merge
base.translation with patch.translation rather than overwriting) so nested
defaults are preserved; update references to validation and merge logic in
validatePartialConfig and mergeConfig accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/website/.env.example`:
- Around line 8-11: The dotenv entries are out of dotenv-linter's expected
alphabetical/order and trigger a lint error; reorder the LITELLM_* variables so
LITELLM_API_KEY appears before LITELLM_BASE_URL (keeping LITELLM_MODEL as
needed) in the .env.example so dotenv-linter passes—update the block containing
LITELLM_API_KEY, LITELLM_BASE_URL, and LITELLM_MODEL accordingly.

In `@scripts/ts-api-extractor/src/config/defaults.ts`:
- Around line 24-36: The translation defaults object in defaults.ts (the
translation block with enabled/targetLocale/llm/validation.mqm) is being
shallow-overwritten by mergeConfig(); update mergeConfig() to perform a
deep/recursive merge for the "translation" key (or use a deep-merge util such as
lodash.merge) so partial user config like translation.validation.mqm.failOnError
only overrides that nested field and preserves defaults.enabled, targetLocale
and llm; locate mergeConfig() and change its logic to deep-merge the incoming
config into the defaults (or add a special-case deep merge for the translation
property) to ensure nested defaults remain intact.

In `@scripts/ts-api-extractor/src/extract.ts`:
- Around line 83-95: The code only assigns writtenFiles to the result of writing
the translated (ko) props, dropping the earlier en outputs; update the logic in
the config.translation?.enabled branch so writtenFiles includes both the
original English writePropsFiles(...) call result and the translated
writePropsFiles(...) result (e.g. combine/concat the returned arrays/counts from
the first writePropsFiles(props, outputDir, formatFileName, 'en') and the later
writePropsFiles(translatedProps, outputDir, formatFileName, 'ko')), leaving
translatePropsInfo and buildReport usage unchanged.

In `@scripts/ts-api-extractor/src/translate/deepl.ts`:
- Around line 8-11: The current DeepL gate returns the original texts when no
API key or on failure, making outages indistinguishable from legitimate
translations; instead, change those early-return paths (the branch checking
apiKey and the other failure branches around lines 30-42) to return undefined
(or null) so the pipeline's deeplResult === undefined check can detect a DeepL
fallback; update any console.warn/error messages to note "falling back (no
DeepL)" and ensure the function that returns texts uses the same undefined
signal across all failure branches.
- Around line 20-28: The DeepL fetch call in the try block should use an
AbortController with a bounded timeout to avoid hanging; create an
AbortController, start a timer (e.g., setTimeout) that calls controller.abort()
after the desired ms, pass controller.signal into the fetch options (in the
request started in this try), and ensure the timeout is cleared in a finally
block (wrap clearTimeout(timeout) inside finally so the timer is always
released) around the fetch/response handling code in deepl.ts.

In `@scripts/ts-api-extractor/src/translate/llm-client.ts`:
- Around line 34-41: The code currently casts response.json() to a shape and
returns data.choices?.[0]?.message?.content without validating its runtime type;
update the extraction around the data and content variables so you check that
data.choices?.[0]?.message?.content is a string (e.g., typeof content ===
'string') before returning it, and if it's missing or not a string return {
content: null, error: 'Unexpected response shape' } instead; adjust the block
that computes content and the subsequent return in llm-client.ts (the
variables/data referenced as data and content) accordingly.
- Around line 12-28: Validate both LITELLM_BASE_URL and LITELLM_API_KEY before
calling fetch (return an error if either is missing rather than sending
Authorization: Bearer undefined), and wrap the fetch call in an AbortController
with a timeout: create an AbortController, pass controller.signal to fetch, set
a timer to call controller.abort() after a configurable timeout, and ensure you
clearTimeout(timeout) in a finally block; update the code around the existing
fetch invocation (the POST to `${baseUrl}/chat/completions` that sends model and
messages) to use the controller.signal and abort/cleanup logic.

In `@scripts/ts-api-extractor/src/translate/llm-postprocess.ts`:
- Around line 35-44: The current post-processing returns result.content after
stripping code fences but if that yields an empty string it mistakenly replaces
a valid deeplDraft; modify the logic in the LLM postprocess block (the code
handling result.content and deeplDraft) to assign the cleaned string to a
variable (e.g., cleaned) after the .replace(...).trim() sequence and then return
cleaned || deeplDraft so that an empty or whitespace-only LLM response falls
back to deeplDraft.

In `@scripts/ts-api-extractor/src/translate/mqm-validator.ts`:
- Around line 53-62: The parsed payload validation in mqm-validator.ts is too
permissive: update the check that currently returns passResult() to also enforce
that (parsed as MqmResult).verdict is exactly "PASS" or "FAIL" and that (parsed
as MqmResult).errors is an array whose entries match the expected error shape
(e.g., each entry is an object with required properties like "message" and
"severity" or whatever fields MqmResult defines); if the verdict is not one of
the two allowed values or any error entry is malformed, log a warning and return
passResult() to avoid trusting malformed payloads. Ensure you reference the
existing symbols parsed, MqmResult, and passResult() when implementing the
stricter validation so downstream logic never receives arbitrary strings like
"OK".

In `@scripts/ts-api-extractor/src/translate/pipeline.ts`:
- Around line 100-116: translateWithDeepl can return fallback originals which
must not be treated as valid DeepL drafts; update translateWithDeepl to return
metadata per item (e.g., { text, usedFallback }) and then in pipeline.ts (the
block using translateWithDeepl, missIndices, deeplResults, postprocessWithLlm
and limit) change the check from deeplResult === undefined to check
deeplResult.usedFallback (or similar) and when usedFallback is true log a
warning and return the source entries[entryIndex].text without calling
postprocessWithLlm; ensure all callers of translateWithDeepl are updated to
handle the new result shape.
- Around line 132-158: When an MQM FAIL occurs the code currently never records
the failure into mqmErrorsByEntryIdx and the retry path can delete errors
incorrectly; update the flow in the block handling mqmResult.verdict === 'FAIL'
so that you (1) record the initial failure via
mqmErrorsByEntryIdx.set(entryIndex, mqmResult.errors), (2) when you run the
retry via recheck = await validateWithMqm(...) replace the stored errors with
recheck.errors if recheck.verdict === 'FAIL', or delete mqmErrorsByEntryIdx
entry only if recheck.verdict !== 'FAIL' (i.e. retry passed), and (3) in the
failOnError true branch before throwing ensure mqmErrorsByEntryIdx contains the
final recheck.errors so the report reflects the ultimate outcome; use the
existing symbols mqmErrorsByEntryIdx, entryIndex, mqmResult, recheck,
validateWithMqm and config.validation.mqm.failOnError to locate and implement
these changes.

In `@scripts/ts-api-extractor/src/translate/report.ts`:
- Around line 21-27: buildReport currently uses failCount computed from MQM
error arrays (errors.length) which overcounts failures because one text can
yield multiple MqmError entries; update buildReport (and any aggregation logic
that sets failCount in TranslationReport) to either (A) compute a distinct
failed-text count by de-duplicating failures per text id (e.g., mark a
component's failedTextCount as count of unique text identifiers with errors and
sum those into failCount) or (B) rename/keep failCount as mqmErrorCount and add
a separate failedTexts field used for pass-rate math; adjust totalTexts,
pass-rate calculation, and FAIL (${failCount}/${totalTexts}) rendering to use
the new failedTexts metric instead of raw MQM error counts (reference
buildReport, failCount, totalTexts, components, and ComponentReport where errors
are stored).
- Around line 33-41: The loop that builds Markdown lines in translate/report.ts
currently only escapes backticks/brackets for source, translation and message,
which allows newlines and other Markdown control characters to inject headings
or list items; update the c.errors iteration (where source, translation, message
are computed and used in lines.push) to run all three values through a sanitizer
(e.g., sanitizeMarkdown) that 1) strips or replaces newlines with spaces, and 2)
escapes common Markdown/control chars (#, -, *, _, >, `, [, ], (, ), +, and
leading/trailing whitespace) so the final template string in lines.push cannot
create headings or bullets. Implement the sanitizer as a small helper and call
it for e.source, e.translation and e.message before formatting the output.

In `@scripts/ts-api-extractor/test/translate/cache.test.ts`:
- Around line 116-120: The test "does not throw when outputDir is unwritable
(logs warning instead)" is non-deterministic; instead create a real temporary
file (e.g., via fs.writeFileSync on a unique tmp path) and pass that file path
into saveCache so the subsequent mkdirSync(dirname(...)) in saveCache will
reliably fail; assert saveCache does not throw, that console.warn was called,
and then remove the temp file in cleanup. Reference: saveCache and the
mkdirSync(dirname(...)) behavior to locate where to trigger the failure.

In `@scripts/ts-api-extractor/test/translate/pipeline.test.ts`:
- Around line 121-154: The test currently only checks that a warning was emitted
and a result exists but does not assert the MQM report contents; update the test
for the FAIL path (the test using translatePropsInfo, sampleProps, baseConfig
and mqmModule.mockResolvedValue) to assert the generated componentReports
contain the MQM failure details by verifying
result.componentReports[0].failCount is > 0 (or equals 1) and that
result.componentReports[0].errors includes the mocked error (e.g. an entry with
severity 'major' and type 'mistranslation'); do the same for the other similar
tests mentioned (lines ~192-231) so the spec verifies the report data consumed
by .i18n-report.md rather than only the warning/result presence.

In `@scripts/ts-api-extractor/test/translate/report.test.ts`:
- Around line 147-158: The test using chmod to make readonlyDir non-writable is
flaky; instead make the write failure deterministic by creating a file where the
directory is expected so mkdirSync(dirname(filePath)) fails: in the 'warns and
does not throw when write fails' test, replace creating a directory with mode
0o444 with creating a regular file at readonlyDir (or creating a file at
path.join(tmpDir, 'readonly', 'placeholder') so the parent mkdir will fail),
call buildReport and pass that path into writeReport as before, assert the same
warnSpy behavior, then restore the spy and remove the file to clean up;
reference functions/variables writeReport, buildReport, readonlyDir, tmpDir,
warnSpy, and the mkdirSync(dirname(filePath)) failure point when making the
change.

---

Outside diff comments:
In @.claude/skills/write-component-jsdocs/references/guide.md:
- Around line 427-468: Update the JSDoc in the Complete Example to follow the
new single-line component and event wording patterns: change the Button
component summary to a single sentence following "A [noun phrase] for/that
[purpose]." and end with the rendered element (e.g., "A button for primary user
actions that renders a <button> element."), and change the onClick prop JSDoc in
ButtonProps to the event handler pattern "Event handler called when [exact
condition]." (e.g., "Event handler called when the button is clicked or
activated via Enter or Space key."). Ensure these changes are applied to the
Button (component summary) and onClick (prop) entries.

In `@scripts/ts-api-extractor/src/config/schema.ts`:
- Around line 37-96: validatePartialConfig currently skips any checks for
config.translation and mergeConfig shallowly replaces translation, allowing
invalid or partial shapes to slip through; add runtime validation inside
validatePartialConfig for the translation object and its nested keys (check
translation.enabled boolean, translation.targetLocale string if present,
translation.llm.enabled boolean if llm present, and
translation.validation.mqm.enabled and translation.validation.mqm.failOnError
booleans when validation/mqm are present) and make mergeConfig perform a
deep-merge for the translation key (merge base.translation with
patch.translation rather than overwriting) so nested defaults are preserved;
update references to validation and merge logic in validatePartialConfig and
mergeConfig accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a6fbd0c-3cca-475d-b9de-1d57cbe1fea2

📥 Commits

Reviewing files that changed from the base of the PR and between 250b914 and 4496269.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (24)
  • .claude/skills/write-component-jsdocs/SKILL.md
  • .claude/skills/write-component-jsdocs/references/guide.md
  • apps/website/.env.example
  • scripts/ts-api-extractor/package.json
  • scripts/ts-api-extractor/src/cli/index.ts
  • scripts/ts-api-extractor/src/config/defaults.ts
  • scripts/ts-api-extractor/src/config/schema.ts
  • scripts/ts-api-extractor/src/extract.ts
  • scripts/ts-api-extractor/src/models/output.ts
  • scripts/ts-api-extractor/src/stages/write.ts
  • scripts/ts-api-extractor/src/translate/cache.ts
  • scripts/ts-api-extractor/src/translate/deepl.ts
  • scripts/ts-api-extractor/src/translate/llm-client.ts
  • scripts/ts-api-extractor/src/translate/llm-postprocess.ts
  • scripts/ts-api-extractor/src/translate/mqm-validator.ts
  • scripts/ts-api-extractor/src/translate/pipeline.ts
  • scripts/ts-api-extractor/src/translate/report.ts
  • scripts/ts-api-extractor/src/translate/types.ts
  • scripts/ts-api-extractor/test/translate/cache.test.ts
  • scripts/ts-api-extractor/test/translate/deepl.test.ts
  • scripts/ts-api-extractor/test/translate/llm-postprocess.test.ts
  • scripts/ts-api-extractor/test/translate/mqm-validator.test.ts
  • scripts/ts-api-extractor/test/translate/pipeline.test.ts
  • scripts/ts-api-extractor/test/translate/report.test.ts
💤 Files with no reviewable changes (1)
  • scripts/ts-api-extractor/src/models/output.ts

Comment thread scripts/ts-api-extractor/src/config/defaults.ts Outdated
Comment thread scripts/ts-api-extractor/src/extract.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/deepl.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/deepl.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/llm-client.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/report.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/report.ts Outdated
Comment thread scripts/ts-api-extractor/test/translate/cache.test.ts Outdated
Comment thread scripts/ts-api-extractor/test/translate/pipeline.test.ts Outdated
Comment thread scripts/ts-api-extractor/test/translate/report.test.ts
…pipeline

- extract.ts: include both en and ko writtenFiles in translate mode
- deepl.ts: return undefined on failure (signal fallback), add 10s AbortController timeout
- llm-client.ts: fail fast on missing LITELLM_API_KEY, add 30s timeout, strict typeof content check
- llm-postprocess.ts: fallback to deeplDraft when cleaned LLM output is empty
- mqm-validator.ts: enforce verdict === 'PASS'|'FAIL' and validate error entry shape
- pipeline.ts: record MQM failures into mqmErrorsByEntryIdx before retry; clear only on retry success; count failed texts (not error entries) for failCount
- report.ts: sanitize Markdown special chars and newlines before injecting into report
- tests: update deepl expectations to undefined, make cache/report write-failure tests deterministic, assert componentReports MQM content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
scripts/ts-api-extractor/src/translate/pipeline.ts (1)

152-165: ⚠️ Potential issue | 🟡 Minor

Report accuracy when failOnError is false.

When failOnError is false, the retry runs (lines 144-150) but no recheck validates whether the retry succeeded. The errors recorded at line 142 persist in the report regardless of whether the retry actually fixed the translation.

Consider running the recheck unconditionally and only throwing when failOnError && recheck.verdict === 'FAIL'. This ensures the report accurately reflects the final translation quality.

🔧 Proposed fix
                         translated = await limit(() =>
                             postprocessWithLlm(
                                 entry.text,
                                 deeplDraft ?? entry.text,
                                 mqmResult.errors,
                             ),
                         );

-                        if (config.validation.mqm.failOnError) {
-                            const recheck = await limit(() =>
-                                validateWithMqm(entry.text, translated, config),
-                            );
-                            if (recheck.verdict === 'FAIL') {
+                        const recheck = await limit(() =>
+                            validateWithMqm(entry.text, translated, config),
+                        );
+
+                        if (recheck.verdict === 'FAIL') {
+                            mqmErrorsByEntryIdx.set(entryIndex, recheck.errors);
+                            if (config.validation.mqm.failOnError) {
                                 mqmErrorsByEntryIdx.set(entryIndex, recheck.errors);
                                 throw new Error(
                                     `[mqm-validator] Translation validation FAILED after retry for: "${entry.text.slice(0, 60)}..."`,
                                 );
-                            } else {
-                                mqmErrorsByEntryIdx.delete(entryIndex);
                             }
+                        } else {
+                            mqmErrorsByEntryIdx.delete(entryIndex);
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/src/translate/pipeline.ts` around lines 152 - 165,
When a retry is performed, always run the recheck via validateWithMqm (using the
same limit(...) call) and update mqmErrorsByEntryIdx based on recheck.verdict;
only throw an Error when config.validation.mqm.failOnError is true AND
recheck.verdict === 'FAIL'. In other words, call validateWithMqm unconditionally
for the retry, set mqmErrorsByEntryIdx.set(entryIndex, recheck.errors) if
recheck.verdict === 'FAIL' else mqmErrorsByEntryIdx.delete(entryIndex), and only
throw inside the block when config.validation.mqm.failOnError && recheck.verdict
=== 'FAIL' (references: validateWithMqm, mqmErrorsByEntryIdx,
config.validation.mqm.failOnError, limit).
🧹 Nitpick comments (3)
scripts/ts-api-extractor/test/translate/deepl.test.ts (2)

10-10: Remove unused DEEPL_GLOSSARY_ID env stubs in this suite.

Line 10 and Line 37 stub an env var that translateWithDeepl does not read (it uses the glossaryId argument). Keeping these stubs can mislead future readers about behavior under test.

Diff suggestion
     beforeEach(() => {
         vi.stubGlobal('fetch', vi.fn());
         vi.stubEnv('DEEPL_ENDPOINT', 'https://api-free.deepl.com/v2/translate');
         vi.stubEnv('DEEPL_API_KEY', 'test-api-key');
-        vi.stubEnv('DEEPL_GLOSSARY_ID', '');
     });
@@
     it('glossary_id 미설정 시 body에 glossary_id 없음', async () => {
-        vi.stubEnv('DEEPL_GLOSSARY_ID', '');
-
         const mockFetch = vi.mocked(fetch);

Also applies to: 37-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/test/translate/deepl.test.ts` at line 10, The test
suite stubs DEEPL_GLOSSARY_ID even though translateWithDeepl reads the
glossaryId argument (not that env var); remove the unnecessary
vi.stubEnv('DEEPL_GLOSSARY_ID', '') calls from this test file (the occurrences
near the top of the suite and the one around line 37) so tests no longer mock an
unused environment variable and avoid misleading readers—leave other stubs
intact and ensure translateWithDeepl invocation still passes the intended
glossaryId argument.

81-92: Add an explicit non-OK HTTP response test for the fallback branch.

You already test rejected fetch; adding a response.ok === false case will directly lock coverage for the status-based fallback path in deepl.ts.

Diff suggestion
     it('fetch 실패 시 undefined 반환 + warn', async () => {
         const mockFetch = vi.mocked(fetch);
         mockFetch.mockRejectedValueOnce(new Error('Network error'));
         const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

         const texts = ['Hello'];
         const result = await translateWithDeepl(texts, '');

         expect(result).toBeUndefined();
         expect(warnSpy).toHaveBeenCalledOnce();
     });
+
+    it('HTTP non-ok 응답 시 undefined 반환 + warn', async () => {
+        const mockFetch = vi.mocked(fetch);
+        mockFetch.mockResolvedValueOnce({
+            ok: false,
+            status: 429,
+            json: async () => ({}),
+        } as Response);
+        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
+
+        const result = await translateWithDeepl(['Hello'], '');
+
+        expect(result).toBeUndefined();
+        expect(warnSpy).toHaveBeenCalledOnce();
+    });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/test/translate/deepl.test.ts` around lines 81 - 92,
Add a test in deepl.test.ts that covers the non-OK HTTP response fallback: mock
the global fetch used by translateWithDeepl so it resolves to a response-like
object with ok: false (and a status/code/message), spy on console.warn, call
translateWithDeepl(['Hello'], ''), and assert the function returns undefined and
console.warn was called; this will exercise the status-based branch in deepl.ts
instead of the rejected-fetch branch already covered.
scripts/ts-api-extractor/src/translate/llm-client.ts (1)

26-26: Normalize trailing slash from baseUrl.

If LITELLM_BASE_URL ends with a trailing slash (e.g., https://api.example.com/), the constructed URL becomes https://api.example.com//chat/completions with a double slash. While most servers handle this gracefully, normalizing prevents potential routing issues.

🔧 Proposed fix
-            response = await fetch(`${baseUrl}/chat/completions`, {
+            response = await fetch(`${baseUrl.replace(/\/+$/, '')}/chat/completions`, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/src/translate/llm-client.ts` at line 26, The fetch
URL can end up with a double slash when LITELLM_BASE_URL has a trailing slash;
normalize baseUrl before using it in the fetch call by trimming any trailing
slashes (e.g., compute baseUrl = LITELLM_BASE_URL.replace(/\/+$/, "") or
similar) and then use that normalized baseUrl in the existing
fetch(`${baseUrl}/chat/completions`, ...) call (update the code around the
fetch/response logic in llm-client.ts to use the trimmed baseUrl variable).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/ts-api-extractor/src/translate/pipeline.ts`:
- Around line 152-165: When a retry is performed, always run the recheck via
validateWithMqm (using the same limit(...) call) and update mqmErrorsByEntryIdx
based on recheck.verdict; only throw an Error when
config.validation.mqm.failOnError is true AND recheck.verdict === 'FAIL'. In
other words, call validateWithMqm unconditionally for the retry, set
mqmErrorsByEntryIdx.set(entryIndex, recheck.errors) if recheck.verdict ===
'FAIL' else mqmErrorsByEntryIdx.delete(entryIndex), and only throw inside the
block when config.validation.mqm.failOnError && recheck.verdict === 'FAIL'
(references: validateWithMqm, mqmErrorsByEntryIdx,
config.validation.mqm.failOnError, limit).

---

Nitpick comments:
In `@scripts/ts-api-extractor/src/translate/llm-client.ts`:
- Line 26: The fetch URL can end up with a double slash when LITELLM_BASE_URL
has a trailing slash; normalize baseUrl before using it in the fetch call by
trimming any trailing slashes (e.g., compute baseUrl =
LITELLM_BASE_URL.replace(/\/+$/, "") or similar) and then use that normalized
baseUrl in the existing fetch(`${baseUrl}/chat/completions`, ...) call (update
the code around the fetch/response logic in llm-client.ts to use the trimmed
baseUrl variable).

In `@scripts/ts-api-extractor/test/translate/deepl.test.ts`:
- Line 10: The test suite stubs DEEPL_GLOSSARY_ID even though translateWithDeepl
reads the glossaryId argument (not that env var); remove the unnecessary
vi.stubEnv('DEEPL_GLOSSARY_ID', '') calls from this test file (the occurrences
near the top of the suite and the one around line 37) so tests no longer mock an
unused environment variable and avoid misleading readers—leave other stubs
intact and ensure translateWithDeepl invocation still passes the intended
glossaryId argument.
- Around line 81-92: Add a test in deepl.test.ts that covers the non-OK HTTP
response fallback: mock the global fetch used by translateWithDeepl so it
resolves to a response-like object with ok: false (and a status/code/message),
spy on console.warn, call translateWithDeepl(['Hello'], ''), and assert the
function returns undefined and console.warn was called; this will exercise the
status-based branch in deepl.ts instead of the rejected-fetch branch already
covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e3ab9178-e82b-492c-8244-c47250744428

📥 Commits

Reviewing files that changed from the base of the PR and between 4496269 and 9a36b5a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (16)
  • .claude/skills/write-component-jsdocs/SKILL.md
  • scripts/ts-api-extractor/src/config/schema.ts
  • scripts/ts-api-extractor/src/extract.ts
  • scripts/ts-api-extractor/src/translate/cache.ts
  • scripts/ts-api-extractor/src/translate/deepl.ts
  • scripts/ts-api-extractor/src/translate/llm-client.ts
  • scripts/ts-api-extractor/src/translate/llm-postprocess.ts
  • scripts/ts-api-extractor/src/translate/mqm-validator.ts
  • scripts/ts-api-extractor/src/translate/pipeline.ts
  • scripts/ts-api-extractor/src/translate/report.ts
  • scripts/ts-api-extractor/test/translate/cache.test.ts
  • scripts/ts-api-extractor/test/translate/deepl.test.ts
  • scripts/ts-api-extractor/test/translate/llm-postprocess.test.ts
  • scripts/ts-api-extractor/test/translate/mqm-validator.test.ts
  • scripts/ts-api-extractor/test/translate/pipeline.test.ts
  • scripts/ts-api-extractor/test/translate/report.test.ts
✅ Files skipped from review due to trivial changes (3)
  • .claude/skills/write-component-jsdocs/SKILL.md
  • scripts/ts-api-extractor/src/translate/cache.ts
  • scripts/ts-api-extractor/src/translate/report.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/ts-api-extractor/src/translate/deepl.ts
  • scripts/ts-api-extractor/test/translate/pipeline.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/ts-api-extractor/src/config/schema.ts (1)

47-94: ⚠️ Potential issue | 🟠 Major

validatePartialConfig should validate translation payload too.

translation was added to config shape, but runtime validation still skips it. Invalid values (e.g., non-boolean flags or unsupported locale) currently pass through and get merged.

🛠️ Proposed fix
 export function validatePartialConfig(config: PartialExtractorConfig): void {
@@
     if (config.verbose !== undefined && typeof config.verbose !== 'boolean') {
         throw new Error('Invalid verbose: expected boolean');
     }
+
+    if (config.translation !== undefined) {
+        if (typeof config.translation !== 'object' || config.translation === null) {
+            throw new Error('Invalid translation: expected object');
+        }
+        const t = config.translation;
+        if (t.enabled !== undefined && typeof t.enabled !== 'boolean') {
+            throw new Error('Invalid translation.enabled: expected boolean');
+        }
+        if (t.targetLocale !== undefined && t.targetLocale !== 'ko') {
+            throw new Error("Invalid translation.targetLocale: expected 'ko'");
+        }
+        if (t.llm !== undefined) {
+            if (typeof t.llm !== 'object' || t.llm === null) {
+                throw new Error('Invalid translation.llm: expected object');
+            }
+            if (t.llm.enabled !== undefined && typeof t.llm.enabled !== 'boolean') {
+                throw new Error('Invalid translation.llm.enabled: expected boolean');
+            }
+        }
+        if (t.validation !== undefined) {
+            if (typeof t.validation !== 'object' || t.validation === null) {
+                throw new Error('Invalid translation.validation: expected object');
+            }
+            if (t.validation.mqm !== undefined) {
+                if (typeof t.validation.mqm !== 'object' || t.validation.mqm === null) {
+                    throw new Error('Invalid translation.validation.mqm: expected object');
+                }
+                if (
+                    t.validation.mqm.enabled !== undefined &&
+                    typeof t.validation.mqm.enabled !== 'boolean'
+                ) {
+                    throw new Error('Invalid translation.validation.mqm.enabled: expected boolean');
+                }
+                if (
+                    t.validation.mqm.failOnError !== undefined &&
+                    typeof t.validation.mqm.failOnError !== 'boolean'
+                ) {
+                    throw new Error(
+                        'Invalid translation.validation.mqm.failOnError: expected boolean',
+                    );
+                }
+            }
+        }
+    }
 
     if (config.components !== undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/src/config/schema.ts` around lines 47 - 94,
validatePartialConfig currently skips the new translation property; add
validation inside validatePartialConfig to ensure config.translation (when
defined) is an object (not null), validate any boolean flags on it using typeof
checks (e.g., translation.enabled, translation.optionalFlag) and validate locale
fields as strings or arrays of strings using the existing assertStringArray
helper; also verify locale values against the known supported locales list
(reference the project’s supported locales constant or enum) and throw
descriptive Errors (e.g., "Invalid translation: expected object", "Invalid
translation.enabled: expected boolean", "Invalid translation.locales:
unsupported locale") so invalid translation payloads are rejected before merge.
♻️ Duplicate comments (1)
scripts/ts-api-extractor/src/translate/report.ts (1)

40-40: ⚠️ Potential issue | 🟡 Minor

Sanitize component names before injecting into markdown headings.

c.name is currently rendered raw in a heading; special markdown characters can distort the report layout.

🛠️ Proposed fix
-    const lines = [`### ${c.name} — ${status}`];
+    const lines = [`### ${sanitizeMarkdown(c.name)} — ${status}`];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-api-extractor/src/translate/report.ts` at line 40, The heading
injection uses c.name raw (const lines = [`### ${c.name} — ${status}`]) which
can break Markdown; implement and call a sanitizer (e.g., escapeMarkdown or
markdownEscape) to escape Markdown-special characters in c.name before building
lines, or wrap the name in a safe code/span (e.g., inline code) so the generated
heading becomes `### ${escapeMarkdown(c.name)} — ${status}`; update the function
that builds the report lines to use this helper wherever c.name is interpolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/ts-api-extractor/.env.example`:
- Around line 12-13: The environment variable entries are out-of-order for the
dotenv-linter rule: swap the two lines so LITELLM_API_KEY appears before
LITELLM_BASE_URL in the .env.example file; update the entries for the symbols
LITELLM_API_KEY and LITELLM_BASE_URL accordingly so the API key line comes
first.

In `@scripts/ts-api-extractor/src/translate/mqm-validator.ts`:
- Around line 73-85: The current guard only checks explanation and severity,
allowing malformed MqmError entries to slip through; update the validator that
returns MqmResult to perform a strict type guard over errors[] (the MqmError
shape) by verifying each entry has non-null object fields source_span and
mt_span (with their expected subfields), a category value matching the MqmError
category enum, and severity/explanation as strings; if any entry fails, log the
unexpected shape and return passResult() instead of casting, and ensure the
function that returns parsed as MqmResult uses this stronger predicate before
the cast.

In `@scripts/ts-api-extractor/src/translate/pipeline.ts`:
- Around line 220-232: The PASS/FAIL decision should use mqmResult.verdict
instead of mqmResult.errors.length; update the control in the mt-only branch to
check mqmResult.verdict (e.g., mqmResult.verdict === 'PASS') so a FAIL verdict
with an empty/partial errors array doesn't get treated as PASS, and ensure the
returned object fields (pipeline, hadErrors, hadOverEdit) are set consistently
when verdict !== 'PASS' (and continue to populate mqmErrorsByEntryIdx and
allowedEditSpans only for non-PASS cases using mqmResult.errors).

---

Outside diff comments:
In `@scripts/ts-api-extractor/src/config/schema.ts`:
- Around line 47-94: validatePartialConfig currently skips the new translation
property; add validation inside validatePartialConfig to ensure
config.translation (when defined) is an object (not null), validate any boolean
flags on it using typeof checks (e.g., translation.enabled,
translation.optionalFlag) and validate locale fields as strings or arrays of
strings using the existing assertStringArray helper; also verify locale values
against the known supported locales list (reference the project’s supported
locales constant or enum) and throw descriptive Errors (e.g., "Invalid
translation: expected object", "Invalid translation.enabled: expected boolean",
"Invalid translation.locales: unsupported locale") so invalid translation
payloads are rejected before merge.

---

Duplicate comments:
In `@scripts/ts-api-extractor/src/translate/report.ts`:
- Line 40: The heading injection uses c.name raw (const lines = [`### ${c.name}
— ${status}`]) which can break Markdown; implement and call a sanitizer (e.g.,
escapeMarkdown or markdownEscape) to escape Markdown-special characters in
c.name before building lines, or wrap the name in a safe code/span (e.g., inline
code) so the generated heading becomes `### ${escapeMarkdown(c.name)} —
${status}`; update the function that builds the report lines to use this helper
wherever c.name is interpolated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 778f6632-b235-4958-a327-90f61210067c

📥 Commits

Reviewing files that changed from the base of the PR and between 9a36b5a and 94840b7.

📒 Files selected for processing (13)
  • scripts/ts-api-extractor/.env.example
  • scripts/ts-api-extractor/src/config/schema.ts
  • scripts/ts-api-extractor/src/translate/cache.ts
  • scripts/ts-api-extractor/src/translate/llm-postprocess.ts
  • scripts/ts-api-extractor/src/translate/mqm-validator.ts
  • scripts/ts-api-extractor/src/translate/pipeline.ts
  • scripts/ts-api-extractor/src/translate/report.ts
  • scripts/ts-api-extractor/src/translate/types.ts
  • scripts/ts-api-extractor/test/translate/cache.test.ts
  • scripts/ts-api-extractor/test/translate/llm-postprocess.test.ts
  • scripts/ts-api-extractor/test/translate/mqm-validator.test.ts
  • scripts/ts-api-extractor/test/translate/pipeline.test.ts
  • scripts/ts-api-extractor/test/translate/report.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • scripts/ts-api-extractor/src/translate/llm-postprocess.ts
  • scripts/ts-api-extractor/test/translate/cache.test.ts
  • scripts/ts-api-extractor/test/translate/report.test.ts
  • scripts/ts-api-extractor/src/translate/cache.ts
  • scripts/ts-api-extractor/test/translate/mqm-validator.test.ts

Comment thread scripts/ts-api-extractor/.env.example Outdated
Comment thread scripts/ts-api-extractor/src/translate/mqm-validator.ts Outdated
Comment thread scripts/ts-api-extractor/src/translate/pipeline.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/website/package.json (1)

1-17: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

웹사이트 패키지에 Node/PNPM 버전 제약을 명시해 주세요.

현재 apps/website/package.jsonengines.nodepackageManager가 없어 환경 드리프트가 발생할 수 있습니다.

제안 수정안
 {
     "name": "website",
     "version": "1.0.0",
     "private": true,
     "type": "module",
+    "packageManager": "pnpm@10.5.1",
+    "engines": {
+        "node": ">=20.19"
+    },
     "scripts": {

As per coding guidelines, apps/website/**/{.nvmrc,.node-version,package.json}는 Node.js v20.19+를 요구하고, apps/website/**/{pnpm-lock.yaml,package.json}는 PNPM v10.5.1+ 사용을 요구합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/package.json` around lines 1 - 17, The apps/website/package.json
file is missing engine version constraints that enforce Node.js and PNPM
requirements, which can lead to environment drift. Add an `engines` field to
specify the minimum Node.js version requirement (v20.19+) and a `packageManager`
field to specify the required PNPM version (v10.5.1+) at the root level of the
package.json object. These fields will ensure all developers and CI environments
use compatible versions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/translation-pipeline/CLAUDE.md`:
- Around line 33-41: The code fence in the architecture diagram block lacks a
language specification, which triggers a markdownlint warning. Add the language
identifier `text` to the opening code fence delimiter so it reads ```text
instead of just ```. This applies to the code block starting with the cli/run.ts
architecture diagram showing the translation pipeline flow.

In `@scripts/translation-pipeline/README.md`:
- Around line 7-15: The code fence blocks in the README.md file are missing
language identifiers, which triggers markdownlint warnings. Add the language
identifier `text` to both code fences: the first one containing the pipeline
flow diagram showing packages/core components through .i18n-report.md, and the
second one showing the directory structure with cli/, translator/, translation/,
validation/, postprocess/, cache/, report/, and the configuration files
(defaults.ts and types.ts). Update the opening triple backticks from just ``` to
```text for both blocks.

In `@scripts/ts-api-extractor/package.json`:
- Around line 38-43: The `@vitest/coverage-v8` dependency is pinned to `^2.1.9`
while `vitest` has been upgraded to `^3.0.0`, creating a peer dependency
mismatch that will cause the test:coverage workflow to fail. Update the version
of `@vitest/coverage-v8` from `^2.1.9` to `^3.0.0` to align with the `vitest`
major version and ensure compatibility.

---

Outside diff comments:
In `@apps/website/package.json`:
- Around line 1-17: The apps/website/package.json file is missing engine version
constraints that enforce Node.js and PNPM requirements, which can lead to
environment drift. Add an `engines` field to specify the minimum Node.js version
requirement (v20.19+) and a `packageManager` field to specify the required PNPM
version (v10.5.1+) at the root level of the package.json object. These fields
will ensure all developers and CI environments use compatible versions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca9241f5-f753-41a6-af28-3223a1677f0e

📥 Commits

Reviewing files that changed from the base of the PR and between ec4a6d0 and b96d2f4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (7)
  • apps/website/package.json
  • handoff.md
  • pnpm-workspace.yaml
  • scripts/translation-pipeline/CLAUDE.md
  • scripts/translation-pipeline/README.md
  • scripts/translation-pipeline/package.json
  • scripts/ts-api-extractor/package.json
💤 Files with no reviewable changes (1)
  • scripts/translation-pipeline/package.json
✅ Files skipped from review due to trivial changes (1)
  • handoff.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • pnpm-workspace.yaml
🛑 Comments failed to post (3)
scripts/translation-pipeline/CLAUDE.md (1)

33-41: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 펜스에 언어를 지정하세요.

이 아키텍처 다이어그램 블록은 언어가 없어 markdownlint 경고가 납니다. text를 붙여 주세요.

수정 예시
-```
+```text
 cli/run.ts
   → translator/translator.ts          # cache lookup, initial translation, outcome merge
       → translation/translate.ts      # LLM initial translation (batch of 20)
       → translator/batch-lifecycle.ts # MQM → postprocess → final MQM
           → validation/validator.ts   # batch MQM evaluation (batch of 10)
           → postprocess/postprocess.ts # corrective rewrite on MQM FAIL
   → report/report.ts                  # renders .i18n-report.md
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 33-33: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/CLAUDE.md` around lines 33 - 41, The code fence
in the architecture diagram block lacks a language specification, which triggers
a markdownlint warning. Add the language identifier `text` to the opening code
fence delimiter so it reads ```text instead of just ```. This applies to the
code block starting with the cli/run.ts architecture diagram showing the
translation pipeline flow.

Source: Linters/SAST tools

scripts/translation-pipeline/README.md (1)

7-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 펜스에 언어를 지정하세요.

두 텍스트 블록 모두 언어가 없어 markdownlint 경고가 납니다. text를 붙여 주세요.

수정 예시
-```
+```text
 packages/core components
     ↓ ts-api-extractor
 generated/en/*.json
     ↓ translation-pipeline
 generated/ko/*.json
 .translation-cache.json
 .i18n-report.md
-```
+```
...
-```
+```text
 src/
 ├── cli/           # Entry point, argument parsing, file I/O
 ├── translator/    # Unit collection, cache lookup, batch lifecycle
 ├── translation/   # LiteLLM calls, initial translation prompt
 ├── validation/    # MQM evaluation prompt and response validation
 ├── postprocess/   # MQM FAIL feedback-based translation correction
 ├── cache/         # SHA256 key generation, cache load/save
 ├── report/        # Per-component stats, report rendering
 ├── defaults.ts    # Default model configuration
 └── types.ts       # Shared type definitions
-```
+```

Also applies to: 86-97

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/README.md` around lines 7 - 15, The code fence
blocks in the README.md file are missing language identifiers, which triggers
markdownlint warnings. Add the language identifier `text` to both code fences:
the first one containing the pipeline flow diagram showing packages/core
components through .i18n-report.md, and the second one showing the directory
structure with cli/, translator/, translation/, validation/, postprocess/,
cache/, report/, and the configuration files (defaults.ts and types.ts). Update
the opening triple backticks from just ``` to ```text for both blocks.

Source: Linters/SAST tools

scripts/ts-api-extractor/package.json (1)

38-43: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Vitest/Coverage provider compatibility from npm metadata
npm view vitest@3.0.0 version peerDependencies
npm view `@vitest/coverage-v8`@2.1.9 version peerDependencies
npm view `@vitest/coverage-v8`@3.0.0 version peerDependencies

Repository: goorm-dev/vapor-ui

Length of output: 439


Update @vitest/coverage-v8 to match vitest major version

Line 43 upgrades vitest to ^3.0.0, but Line 38 keeps @vitest/coverage-v8 at ^2.1.9. According to npm peer dependencies, @vitest/coverage-v8@2.1.9 requires vitest@2.1.9, while @vitest/coverage-v8@3.0.0 requires vitest@3.0.0. This mismatch will cause failures in the test:coverage workflow.

Suggested fix
-        "`@vitest/coverage-v8`": "^2.1.9",
+        "`@vitest/coverage-v8`": "^3.0.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        "`@vitest/coverage-v8`": "^3.0.0",
        "eslint": "^9.39.4",
        "tsup": "^8.5.1",
        "tsx": "^4.21.0",
        "typescript": "catalog:",
        "vitest": "^3.0.0"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ts-api-extractor/package.json` around lines 38 - 43, The
`@vitest/coverage-v8` dependency is pinned to `^2.1.9` while `vitest` has been
upgraded to `^3.0.0`, creating a peer dependency mismatch that will cause the
test:coverage workflow to fail. Update the version of `@vitest/coverage-v8` from
`^2.1.9` to `^3.0.0` to align with the `vitest` major version and ensure
compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-component batching

번역 파이프라인의 평가 체계와 실행 구조를 재정비하고, 216개 컴포넌트 문서를 한국어로
전량 번역해 문서 사이트에 연결한다.

## 평가 체계

문자열 단위 LLM 판정은 근거가 약하다(MQM Council의 250단어 하한, WMT25의 세그먼트 레벨
열세, 한국어 단어 단위 오류 탐지율 0%). 그래서 "틀리면 개발자가 잘못 구현하는" 종류의
오류는 LLM에서 떼어내 결정론 체크로 옮겼다.

- `validation/preserve.ts` 신설 — 백틱 코드 스팬 · 백틱 밖 식별자 · URL · 마크다운 구조
- 위반 → 후편집 → 재검사 → 그래도 실패하면 영어 원문 유지(`preservation_fallback`)
- MQM 루브릭 16 → 6 (Accuracy 3 + Fluency 3). 나머지 10축은 결정론 체크가 대신한다
- 평가는 `gemini-3-pro`로 계열을 분리해 self-bias를 막고, 후처리는 `claude-sonnet-4-6`로 내렸다

## 실행 구조

병목은 순차 루프가 아니라 중복 번역이었다. 유닛 1,167건 중 고유 원문은 300건뿐이고
(className 206회 · render 206회 · style 144회 — base-ui 상속 props 복사분), 상위 3건이
전체의 48%를 차지한다.

- 배치 구성 전에 원문으로 중복을 제거하고 결과를 원 유닛 전체에 되뿌린다
- 컴포넌트별 인터리브를 단계 분리(번역 전수 → MQM 전수)로 바꿨다. 컴포넌트 컨텍스트는
  번역에만 값어치가 있어 유닛별 필드로 내렸다
- 배치 단위 동시성 16(손으로 만든 워커 풀). 배치 id는 `getTranslationUnitKey()` —
  `props[0].size.description` 형태로는 컴포넌트를 섞으면 충돌한다
- MQM 배치 20 → 74. 근거는 (타임아웃 − 여유) × 실측 81 tok/s ÷ 유닛당 출력 ÷ 안전계수 2
- 429/5xx/timeout 지수 백오프 2회(1s→4s). 4xx는 재시도하지 않는다

456콜·수 시간·$5~8 → 20콜·3분·$1~2.

## 저장 레이아웃·사이트 연동

- flat JSON 200개를 지우고 `generated/en/`(추출 산출물) + `generated/ko/`(번역 산출물) 병치
- 소비자 2곳은 `ko/`를 읽고 없으면 `en/`으로 폴백한다
- 전량 실행 결과: 1,167건 PASS율 100%, 영어 폴백 0건, 보존 위반 0건(독립 재검사)
@MaxLee-dev MaxLee-dev changed the title feat(translation-pipeline): standalone DeepL + LLM + MQM translation package feat(translation-pipeline): LLM translation pipeline with deterministic preservation gate Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (7)
scripts/translation-pipeline/src/validation/validator.ts (1)

27-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

satisfies는 누락된 카테고리를 잡지 못합니다. 컴파일 타임 완전성 검사를 추가하세요.

line 27의 주석은 이 배열이 MqmCategory 유니온에서 파생된다고 설명합니다. 실제로는 파생되지 않습니다. satisfies MqmCategory[]는 각 원소가 유니온에 속하는지만 검사합니다. 유니온의 모든 멤버가 배열에 있는지는 검사하지 않습니다.

types.ts에 카테고리를 하나 추가하면 이 배열은 그대로 6개로 남고 컴파일 오류는 발생하지 않습니다. 그 결과 batch-lifecycle.ts line 36의 BATCH_MQM_RESPONSE_SCHEMA enum이 옛 6개 값만 허용합니다. strict: true JSON 스키마이므로 LLM은 새 카테고리를 절대 반환할 수 없습니다. isMqmError도 같은 값을 거부합니다. 새 카테고리는 조용히 무효가 됩니다.

키 기반 파생으로 바꾸면 누락이 컴파일 오류가 됩니다.

♻️ 제안 리팩터
-// MqmCategory 유니온에서 파생 — 카테고리 추가/삭제는 types.ts 한 곳에서만
-export const MQM_CATEGORY_VALUES = [
-    'Accuracy/Mistranslation',
-    'Accuracy/Omission',
-    'Accuracy/Addition',
-    'Fluency/Unnatural phrasing',
-    'Fluency/Style inconsistency',
-    'Fluency/Grammatical error',
-] satisfies MqmCategory[];
+// MqmCategory 유니온에서 파생 — 멤버를 빠뜨리면 컴파일 오류가 난다.
+// 카테고리 추가/삭제는 types.ts 한 곳에서만 한다.
+const MQM_CATEGORY_KEYS: Record<MqmCategory, true> = {
+    'Accuracy/Mistranslation': true,
+    'Accuracy/Omission': true,
+    'Accuracy/Addition': true,
+    'Fluency/Unnatural phrasing': true,
+    'Fluency/Style inconsistency': true,
+    'Fluency/Grammatical error': true,
+};
+export const MQM_CATEGORY_VALUES = Object.keys(MQM_CATEGORY_KEYS) as MqmCategory[];
 
-export const MQM_SEVERITY_VALUES = ['minor', 'major', 'critical'] satisfies MqmError['severity'][];
+const MQM_SEVERITY_KEYS: Record<MqmError['severity'], true> = {
+    minor: true,
+    major: true,
+    critical: true,
+};
+export const MQM_SEVERITY_VALUES = Object.keys(MQM_SEVERITY_KEYS) as MqmError['severity'][];

Object.keys의 순서는 문자열 리터럴 키에서 삽입 순서를 따르므로, 프롬프트에 나열된 순서와 일치합니다.

가이드라인에 따라: "MqmCategory in types.ts is the single source of truth for MQM categories."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/validation/validator.ts` around lines 27 -
37, Update MQM_CATEGORY_VALUES to derive its entries from the MqmCategory keys
defined in types.ts, using a key-based construction that enforces compile-time
completeness rather than only validating array members. Preserve the existing
prompt ordering and keep MqmCategory as the single source of truth; ensure
downstream BATCH_MQM_RESPONSE_SCHEMA and isMqmError receive the complete
category list.

Source: Coding guidelines

scripts/translation-pipeline/src/translator/batch-lifecycle.ts (1)

259-308: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

validateBatchWithMqm과 중복된 호출·파싱 골격을 헬퍼로 추출하세요.

postprocessBatchWithLlm(line 259-308)과 validateBatchWithMqm(line 130-172)은 같은 구조를 반복합니다. 빈 입력 단축 반환, callLlm 호출, !result.content 처리와 statusInfo 조합, parseLlmJson, 객체 여부 확인, try/catch 변환이 모두 동일합니다.

두 함수가 각각 오류 메시지를 조립하므로, 한쪽만 수정하면 두 경로의 진단 형식이 갈라집니다.

♻️ 제안 리팩터
+async function callBatchLlm<R>(
+    systemPrompt: string,
+    request: unknown,
+    schema: { name: string; schema: object },
+    model: string,
+    label: string,
+    onParsed: (parsed: Record<string, unknown>) => R,
+    onInvalid: (reason: string) => R,
+): Promise<R> {
+    const result = await callLlm(
+        [
+            { role: 'system', content: systemPrompt },
+            { role: 'user', content: JSON.stringify(request) },
+        ],
+        { model, jsonSchema: schema },
+    );
+
+    if (!result.content) {
+        const statusInfo = result.statusCode !== undefined ? ` (HTTP ${result.statusCode})` : '';
+        return onInvalid(`[${label}] ${result.error ?? 'empty response'}${statusInfo}`);
+    }
+
+    try {
+        const parsed = parseLlmJson(result.content);
+        if (typeof parsed !== 'object' || parsed === null) {
+            return onInvalid(`[${label}] response must be a JSON object`);
+        }
+        return onParsed(parsed as Record<string, unknown>);
+    } catch (error) {
+        const message = error instanceof Error ? error.message : String(error);
+        return onInvalid(`[${label}] failed to parse response: ${message}`);
+    }
+}

두 함수는 요청 조립과 검증 콜백만 남습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts` around lines
259 - 308, Extract the duplicated empty-input handling, callLlm invocation,
missing-content/status formatting, JSON parsing, object validation, and
exception conversion from validateBatchWithMqm and postprocessBatchWithLlm into
a shared helper. Keep each function responsible only for building its request
and validating the parsed response through a callback, while preserving their
existing result types and error messages consistently.
scripts/translation-pipeline/src/translation/client.ts (3)

120-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

하드코딩된 모델 문자열을 DEFAULT_TRANSLATION_MODEL로 교체하세요.

'claude-sonnet-4-6'이 line 121과 line 165에 직접 적혀 있습니다. defaults.tsDEFAULT_TRANSLATION_MODEL도 같은 값을 선언합니다. 값이 세 곳에 중복됩니다.

defaults.ts에서 기본 모델을 바꾸면 client.ts는 옛 값을 계속 사용합니다. options.model을 생략하는 호출자는 의도하지 않은 모델로 요청을 보냅니다. line 165는 그 옛 이름을 result.model로 보고하므로 사용량·비용 집계도 틀어집니다.

♻️ 제안 수정
+import { DEFAULT_TRANSLATION_MODEL } from '~/defaults';
+
 export interface LlmMessage {
-                    model: options.model ?? 'claude-sonnet-4-6',
+                    model: options.model ?? DEFAULT_TRANSLATION_MODEL,
             model:
                 typeof data['model'] === 'string'
                     ? data['model']
-                    : (options.model ?? 'claude-sonnet-4-6'),
+                    : (options.model ?? DEFAULT_TRANSLATION_MODEL),

Also applies to: 160-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translation/client.ts` around lines 120 -
122, Replace both hardcoded 'claude-sonnet-4-6' fallbacks in the translation
client request and result-model reporting with the imported
DEFAULT_TRANSLATION_MODEL from defaults.ts, while preserving options.model
precedence.

81-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

429 재시도에 지터를 추가하고 Retry-After를 반영하세요.

RETRY_DELAYS_MS는 고정 지연입니다. translator.tsforEachWithConcurrency로 여러 배치를 동시에 실행합니다. 게이트웨이가 레이트 리밋에 걸리면 모든 동시 배치가 같은 시점에 429를 받습니다. 그 배치들은 정확히 1초 뒤에, 다시 4초 뒤에 동시에 재시도합니다. 이 동기화된 재시도는 레이트 리밋을 계속 유발합니다.

지연에 무작위 지터를 더하면 재시도 시점이 분산됩니다. 429 응답의 Retry-After 헤더가 있으면 그 값을 우선 사용하세요.

♻️ 제안 리팩터
+function jittered(ms: number): number {
+    return ms + Math.floor(Math.random() * ms * 0.5);
+}
+
 export async function callLlm(
     messages: LlmMessage[],
     options: LlmCallOptions = {},
 ): Promise<LlmCallResult> {
     let result = await callLlmOnce(messages, options);
     for (const wait of RETRY_DELAYS_MS) {
         if (result.content !== null || !isRetryable(result)) return result;
-        await delay(wait);
+        await delay(Math.max(jittered(wait), result.retryAfterMs ?? 0));
         result = await callLlmOnce(messages, options);
     }
     return result;
 }

callLlmOnce의 non-ok 분기에서 retryAfterMs를 채우세요.

         if (!response.ok) {
+            const retryAfter = Number(response.headers.get('retry-after'));
             return {
                 content: null,
                 error: `Request failed with status ${response.status}`,
                 statusCode: response.status,
+                ...(Number.isFinite(retryAfter) && retryAfter > 0
+                    ? { retryAfterMs: retryAfter * 1_000 }
+                    : {}),
             };
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translation/client.ts` around lines 81 - 92,
Update callLlmOnce to capture a 429 response’s Retry-After value as retryAfterMs
in its non-ok result, then update callLlm to prefer that delay and otherwise
apply random jitter to each RETRY_DELAYS_MS value before retrying. Preserve the
existing retryability checks and final-result behavior.

69-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

문자열 접두사 대신 명시적 플래그로 재시도 여부를 판정하세요.

isRetryable은 line 74에서 result.error.startsWith('fetch failed')로 네트워크 실패를 식별합니다. 이 문자열은 line 171의 템플릿에만 의존합니다. 누군가 line 171의 메시지 형식을 바꾸면 네트워크 오류와 타임아웃 재시도가 조용히 사라집니다. 컴파일 오류는 발생하지 않습니다.

LlmCallResult에 판별 필드를 추가하면 이 결합이 사라집니다.

♻️ 제안 리팩터
 export interface LlmCallResult {
     content: string | null;
     error?: string;
+    /** 네트워크 오류·타임아웃 등 재시도 가능한 전송 실패 */
+    transportFailure?: true;
     model?: string;
 function isRetryable(result: LlmCallResult): boolean {
     if (result.statusCode !== undefined) {
         return result.statusCode === 429 || result.statusCode >= 500;
     }
-    // statusCode가 없는 실패는 네트워크 오류·타임아웃(AbortError)
-    return result.error !== undefined && result.error.startsWith('fetch failed');
+    return result.transportFailure === true;
 }
     } catch (error) {
         const message = error instanceof Error ? error.message : String(error);
-        return { content: null, error: `fetch failed: ${message}` };
+        return { content: null, error: `fetch failed: ${message}`, transportFailure: true };
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translation/client.ts` around lines 69 - 75,
Update LlmCallResult and the translation call flow to carry an explicit
retryable-network-error flag, then change isRetryable to use that flag instead
of checking result.error's message prefix. Set the flag for network failures and
AbortError timeouts at the result construction site, while preserving the
existing 429 and 5xx status-code behavior.
scripts/translation-pipeline/src/translator/translator.test.ts (1)

306-363: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

batch_final_mqm_failed 경로를 검증하는 테스트가 없습니다.

이 파일은 batch_mqm_failed(line 184)와 batch_postprocess_failed(line 225)를 검증합니다. 세 번째 실패 사유인 batch_final_mqm_failed는 검증하지 않습니다.

batch-lifecycle.ts line 506-518이 이 사유를 생성합니다. 코딩 가이드라인은 최종 MQM 응답이 잘못되면 해당 유닛을 unverifiedbatch_final_mqm_failed로 표시하도록 요구합니다. 이 경로는 현재 커버리지가 없습니다.

기존 테스트와 같은 llmCallCount 패턴으로 추가할 수 있습니다. 1) 초기 MQM은 FAIL, 2) 후편집은 유효한 번역 반환, 3) 최종 MQM은 잘못된 JSON 반환.

💚 제안 테스트
+    it('marks units as degraded with batch_final_mqm_failed when the final MQM response is invalid', async () => {
+        let llmCallCount = 0;
+        vi.spyOn(clientModule, 'callLlm').mockImplementation(async () => {
+            llmCallCount++;
+            if (llmCallCount === 1) {
+                return {
+                    content: JSON.stringify({
+                        evaluations: [
+                            {
+                                id: '0:component.description',
+                                verdict: 'FAIL',
+                                errors: [
+                                    {
+                                        category: 'Accuracy/Mistranslation',
+                                        severity: 'major',
+                                        source_span: 'A button component.',
+                                        mt_span: 'Button 컴포넌트',
+                                        explanation: '오역입니다.',
+                                    },
+                                ],
+                            },
+                        ],
+                    }),
+                };
+            }
+            if (llmCallCount === 2) {
+                return {
+                    content: JSON.stringify({
+                        translations: [
+                            { id: '0:component.description', translated: '후편집된 번역입니다.' },
+                        ],
+                    }),
+                };
+            }
+            return { content: 'not-valid-json' };
+        });
+        vi.spyOn(console, 'warn').mockImplementation(() => undefined);
+
+        const result = await translatePropsInfo([
+            { name: 'Button', description: 'A button component.', props: [] },
+        ]);
+
+        expect(result.props[0].description).toBe('후편집된 번역입니다.');
+        expect(result.componentReports[0].unverifiedOutcomes[0]).toMatchObject({
+            reason: 'batch_final_mqm_failed',
+            assurance: 'unverified',
+            reportable: true,
+        });
+        expect(result.batchFallbacks[0]?.reason).toContain('final batch MQM invalid');
+    });

이 테스트는 후편집 결과가 캐시에 저장되지 않는 것도 함께 확인합니다.

가이드라인에 따라: "Malformed MQM, postprocess, or final MQM responses must mark affected units unverified with the corresponding reason (batch_mqm_failed, batch_postprocess_failed, or batch_final_mqm_failed)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/translator.test.ts` around lines
306 - 363, 추가 테스트로 batch_final_mqm_failed 경로를 검증하세요. 기존 llmCallCount 패턴을 사용해 초기
MQM은 FAIL, 후편집은 유효한 번역, 최종 MQM은 잘못된 JSON을 반환하도록 모킹하고, 결과 유닛이 원문으로 폴백되지 않으면서
unverified 및 batch_final_mqm_failed로 표시되는지와 후편집 결과가 캐시에 저장되지 않는지 확인하세요.

Source: Coding guidelines

apps/website/src/utils/get-component-doc.ts (1)

22-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

두 파일이 동일한 "ko 우선, en 폴백" 로케일 해석 로직을 각각 구현합니다. 두 곳 모두 번역 파이프라인 산출물 경로 규칙({ko|en}/{componentName}.json)에 의존하지만, 로직이 각 파일에 개별적으로 하드코딩되어 있어 로케일 순서나 경로 구조 변경 시 두 곳을 동기화해야 합니다.

  • apps/website/src/utils/get-component-doc.ts#L22-L35: ['ko', 'en'] 후보 경로 생성과 fs.existsSync 선택 로직을 공유 유틸리티 함수로 추출하십시오.
  • apps/website/src/components/component-props-table/component-props-table.tsx#L39-L43: 동일한 유틸리티를 기반으로 하는 fetch 래퍼(또는 공유 로케일 순서 상수)를 사용하여 로직을 통일하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/utils/get-component-doc.ts` around lines 22 - 35, 두 파일의 중복된
“ko 우선, en 폴백” 로케일 경로 해석을 공유 유틸리티로 추출하십시오.
apps/website/src/utils/get-component-doc.ts의 후보 경로 생성 및 fs.existsSync 선택 로직을 공용
함수로 옮기고,
apps/website/src/components/component-props-table/component-props-table.tsx는 해당
유틸리티를 사용하는 fetch 래퍼 또는 공유 로케일 순서 상수를 통해 동일한 로직을 사용하도록 변경하십시오.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/write-component-jsdocs/SKILL.md:
- Around line 49-54: Keep the default-value documentation requirement consistent
between the “Prop description rules” section and the checklist around the
documented default-value requirement. Restore rule 4 with the established
`@default` usage guidance, or update both locations together if changing the
policy; do not leave the checklist requiring behavior that the rules no longer
explain.

In `@scripts/translation-pipeline/CLAUDE.md`:
- Line 33: 인라인 언어 식별자가 없는 fenced code block의 시작 fence를 text로 지정하십시오.
scripts/translation-pipeline/CLAUDE.md의 33-33행,
scripts/translation-pipeline/README.md의 7-7행과 86-86행에서 각각 시작 fence를 ```text로
변경하십시오.

In `@scripts/translation-pipeline/src/defaults.ts`:
- Around line 1-4: Update DEFAULT_VALIDATION_MODEL in defaults.ts to remove the
deprecated gemini-3-pro label or replace it with a model name currently
registered in the LiteLLM gateway, while leaving DEFAULT_TRANSLATION_MODEL and
DEFAULT_POSTPROCESS_MODEL unchanged.

In `@scripts/translation-pipeline/src/report/report.ts`:
- Around line 45-69: In buildComponentReports, remove the outcomes.get(unit.id)
fallback from the componentOutcomes mapping. Retrieve each result only with
getTranslationUnitKey(unit), matching the key used by the outcomes creation path
and translator.ts while preserving the existing filtering and report
aggregation.

In `@scripts/translation-pipeline/src/translation/client.ts`:
- Around line 107-158: Update the timeout lifecycle in the translation client’s
fetch flow so the AbortController remains active through await response.json(),
and only clear the timeout after the response body has been fully read. Preserve
the existing response validation and result construction, optionally extracting
the current return object into the suggested buildResult helper without changing
its behavior.

In `@scripts/translation-pipeline/src/translation/translate.test.ts`:
- Around line 26-30: Update the tests around mockFetchContent and the affected
test cases to mock ~/translation/client with vi.mock instead of stubbing fetch,
ensuring callLlm is never executed through the real wrapper. Mock callLlm and
assert its message and options arguments, while leaving HTTP payload coverage to
translation/client.test.ts.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts`:
- Around line 105-128: Update validateBatchEvaluations to validate each
reconciled evaluation’s verdict and errors using the exported isMqmError from
validator.ts before constructing the MqmResult map; reject malformed values
through the existing catch path so the batch degrades to invalidMqm and affected
units remain unverified with the corresponding reason.
- Line 19: POSTPROCESS_BATCH_SIZE의 임의 값 10을 제거하고, client.ts의 60초 타임아웃에서 마진을 차감한
시간, 실측 처리량, 유닛당 출력 토큰, 안전 계수를 반영해 산출하세요. 산출에 사용한 측정값과 근거를 POSTPROCESS_BATCH_SIZE
주변 상수와 함께 명시하고, client.ts의 타임아웃 변경 시 translator.ts의 TRANSLATION_BATCH_SIZE 및
MQM_BATCH_SIZE도 같은 기준으로 재계산되도록 연관 값을 갱신하세요.

In `@scripts/translation-pipeline/src/translator/translator.test.ts`:
- Around line 1-7: 모듈 수준에서 translation client 전체를 모킹하도록 설정하세요.
`translator.test.ts`의 import 영역 인근에 `vi.mock('~/translation/client')`를 추가해 모든
테스트에서 실제 `callLlm` 구현과 네트워크 요청이 실행되지 않게 하되, 기존 `vi.spyOn(clientModule,
'callLlm')` 호출은 모킹된 모듈 위에서 계속 사용하세요.
- Around line 21-30: Update mockTranslations to use getTranslationUnitKey(unit)
for the translations fixture lookup, matching the composite key used when
constructing the returned Map. Remove the bare unit.id lookup so units with
identical IDs in different components resolve independently.

In `@scripts/translation-pipeline/src/translator/translator.ts`:
- Around line 12-15: Document the derivation for TRANSLATION_BATCH_SIZE next to
its declaration, including the timeout, margin, measured throughput, output
tokens per unit, and safety factor used to calculate 20. Keep the rationale
consistent with the existing MQM_BATCH_SIZE comment and ensure both batch sizes
can be recalculated if the client timeout changes.

In `@scripts/translation-pipeline/src/validation/preserve.ts`:
- Around line 12-15: Update the preservation validation around
MULTI_HUMP_IDENTIFIER and checkPreservation to accept
TranslationUnit.componentName and prop names from callers, then
deterministically preserve known bare identifiers and hyphenated HTML/ARIA
identifiers such as Button, size, aria-label, and data-state when they appear in
the source. Retain the existing multi-hump behavior while adding coverage for
single-hump and hyphenated cases, including transformed API documentation.

---

Nitpick comments:
In `@apps/website/src/utils/get-component-doc.ts`:
- Around line 22-35: 두 파일의 중복된 “ko 우선, en 폴백” 로케일 경로 해석을 공유 유틸리티로 추출하십시오.
apps/website/src/utils/get-component-doc.ts의 후보 경로 생성 및 fs.existsSync 선택 로직을 공용
함수로 옮기고,
apps/website/src/components/component-props-table/component-props-table.tsx는 해당
유틸리티를 사용하는 fetch 래퍼 또는 공유 로케일 순서 상수를 통해 동일한 로직을 사용하도록 변경하십시오.

In `@scripts/translation-pipeline/src/translation/client.ts`:
- Around line 120-122: Replace both hardcoded 'claude-sonnet-4-6' fallbacks in
the translation client request and result-model reporting with the imported
DEFAULT_TRANSLATION_MODEL from defaults.ts, while preserving options.model
precedence.
- Around line 81-92: Update callLlmOnce to capture a 429 response’s Retry-After
value as retryAfterMs in its non-ok result, then update callLlm to prefer that
delay and otherwise apply random jitter to each RETRY_DELAYS_MS value before
retrying. Preserve the existing retryability checks and final-result behavior.
- Around line 69-75: Update LlmCallResult and the translation call flow to carry
an explicit retryable-network-error flag, then change isRetryable to use that
flag instead of checking result.error's message prefix. Set the flag for network
failures and AbortError timeouts at the result construction site, while
preserving the existing 429 and 5xx status-code behavior.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts`:
- Around line 259-308: Extract the duplicated empty-input handling, callLlm
invocation, missing-content/status formatting, JSON parsing, object validation,
and exception conversion from validateBatchWithMqm and postprocessBatchWithLlm
into a shared helper. Keep each function responsible only for building its
request and validating the parsed response through a callback, while preserving
their existing result types and error messages consistently.

In `@scripts/translation-pipeline/src/translator/translator.test.ts`:
- Around line 306-363: 추가 테스트로 batch_final_mqm_failed 경로를 검증하세요. 기존 llmCallCount
패턴을 사용해 초기 MQM은 FAIL, 후편집은 유효한 번역, 최종 MQM은 잘못된 JSON을 반환하도록 모킹하고, 결과 유닛이 원문으로
폴백되지 않으면서 unverified 및 batch_final_mqm_failed로 표시되는지와 후편집 결과가 캐시에 저장되지 않는지
확인하세요.

In `@scripts/translation-pipeline/src/validation/validator.ts`:
- Around line 27-37: Update MQM_CATEGORY_VALUES to derive its entries from the
MqmCategory keys defined in types.ts, using a key-based construction that
enforces compile-time completeness rather than only validating array members.
Preserve the existing prompt ordering and keep MqmCategory as the single source
of truth; ensure downstream BATCH_MQM_RESPONSE_SCHEMA and isMqmError receive the
complete category list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines 49 to 54
## Prop description rules

1. Don't repeat the prop name or its type
2. Describe side effects and interactions with other props
3. For numeric props: include unit and valid range
4. For event handlers: specify the exact trigger condition, not just "handler"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

기본값 문서화 규칙을 유지하십시오.

@default 사용 규칙을 삭제하면 작성 규칙에는 기본값 표기 방법이 없지만 Line 70 체크리스트에는 해당 요구사항이 남습니다. 규칙 4를 유지하거나 기본값 정책을 두 위치에서 함께 변경하십시오.

🧰 Tools
🪛 SkillSpector (2.4.4)

[warning] 21: [MP2] Context Window Stuffing: Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Remediation: Implement context-window management that detects and rejects padding or stuffing attempts. Prioritize system instructions over user-injected content.

(Memory Poisoning (MP2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/write-component-jsdocs/SKILL.md around lines 49 - 54, Keep
the default-value documentation requirement consistent between the “Prop
description rules” section and the checklist around the documented default-value
requirement. Restore rule 4 with the established `@default` usage guidance, or
update both locations together if changing the policy; do not leave the
checklist requiring behavior that the rules no longer explain.


### Core Flow

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

fenced code block에 언어 식별자를 지정하십시오.

언어 식별자가 없으면 markdownlint MD040 경고가 발생합니다. 다이어그램과 디렉터리 트리에는 text를 사용하십시오.

  • scripts/translation-pipeline/CLAUDE.md#L33-L33: 시작 fence를 ```text로 변경하십시오.
  • scripts/translation-pipeline/README.md#L7-L7: 시작 fence를 ```text로 변경하십시오.
  • scripts/translation-pipeline/README.md#L86-L86: 시작 fence를 ```text로 변경하십시오.
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 33-33: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 2 files
  • scripts/translation-pipeline/CLAUDE.md#L33-L33 (this comment)
  • scripts/translation-pipeline/README.md#L7-L7
  • scripts/translation-pipeline/README.md#L86-L86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/CLAUDE.md` at line 33, 인라인 언어 식별자가 없는 fenced
code block의 시작 fence를 text로 지정하십시오. scripts/translation-pipeline/CLAUDE.md의
33-33행, scripts/translation-pipeline/README.md의 7-7행과 86-86행에서 각각 시작 fence를
```text로 변경하십시오.

Source: Linters/SAST tools

Comment on lines +1 to +4
export const DEFAULT_TRANSLATION_MODEL = 'claude-sonnet-4-6';
// 평가는 번역과 다른 계열로 분리해 self-bias를 막는다 (KAN-10)
export const DEFAULT_VALIDATION_MODEL = 'gemini-3-pro';
export const DEFAULT_POSTPROCESS_MODEL = 'claude-sonnet-4-6';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every hardcoded model identifier and any gateway model config.
rg -n --type=ts -C2 'claude-sonnet|gemini-3|DEFAULT_(TRANSLATION|VALIDATION|POSTPROCESS)_MODEL'
fd -H -t f '.env.example' --exec cat -n {}

Repository: goorm-dev/vapor-ui

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked defaults/client/env files:"
git ls-files | rg '(^|/)defaults\.ts$|(^|/)client\.ts$|(^|/)\.env\.example$|lite|gateway|transform|model' | head -200

echo
echo "Search for model strings with all tracked files:"
rg -n -C 2 'claude-sonnet|gemini-3|DEFAULT_(TRANSLATION|VALIDATION|POSTPROCESS)_MODEL|model:' "$(git ls-files | tr '\n' ' ')" 2>/dev/null | head -300

echo
echo "Search for LiteLLM/gateway config files broadly:"
rg -n -C 2 'litellm|gateway|ollama|proxy|model_list|model_map' . --glob '!node_modules' --glob '!dist' --glob '!build' 2>/dev/null | head -300

Repository: goorm-dev/vapor-ui

Length of output: 24192


🏁 Script executed:

#!/bin/bash
set -u

echo "Translation-pipeline source files:"
git ls-files scripts/translation-pipeline/src | sed -n '1,200p'

echo
echo "defaults.ts:"
cat -n scripts/translation-pipeline/src/defaults.ts

echo
echo "client.ts model-related section:"
sed -n '90,180p' scripts/translation-pipeline/src/translation/client.ts | cat -n

echo
echo "translate.ts call sites:"
rg -n -C 4 'DEFAULT_(TRANSLATION|VALIDATION|POSTPROCESS)_MODEL|translateUnits|model_name|choices|LITELLM' scripts/translation-pipeline/src/translation scripts/translation-pipeline/src/translator tests 2>/dev/null

echo
echo "Model identifier occurrences only:"
rg -n 'claude-sonnet-4-6|gemini-3-pro|DEFAULT_(TRANSLATION|VALIDATION|POSTPROCESS)_MODEL|LITELLM' scripts/translation-pipeline 2>/dev/null

Repository: goorm-dev/vapor-ui

Length of output: 37230


🌐 Web query:

LiteLLM Gemini 2.5 Pro model name routing gemini-3-pro

💡 Result:

As of August 3, 2026, Gemini 2.5 Pro is an established model, and Gemini 3 Pro is a deprecated, legacy model that was discontinued in March 2026 [1][2][3]. LiteLLM supports Gemini 2.5 Pro via the model name gemini-2.5-pro [4][5][6]. When calling the model in LiteLLM, it is recommended to prefix it with the provider name to ensure correct routing [7][8]: - Gemini API: model="gemini/gemini-2.5-pro" [7][5] - Vertex AI: model="vertex_ai/gemini-2.5-pro" [8][9] Regarding your reference to gemini-3-pro, this model was deprecated and shut down by Google in March 2026 [1][2][3]. It has been superseded by newer models in the Gemini 3.1 series [2][10][11]. If you are looking to use current Gemini 3-series models in LiteLLM, you should use the updated model names such as gemini-3.1-pro-preview or other available variants listed in the official LiteLLM documentation [12][8][13].

Citations:


🌐 Web query:

LiteLLM Claude 4 Sonnet model name claude-sonnet-4-6

💡 Result:

Yes, "claude-sonnet-4-6" is a supported model name in LiteLLM [1][2]. It refers to the Claude Sonnet 4.6 model released by Anthropic [3][4]. LiteLLM provides support for this model across various providers, including Anthropic, Azure AI Foundry, Google Vertex AI, and Amazon Bedrock [2]. Depending on the provider, you may need to use specific prefixes or model IDs in your LiteLLM configuration [2]: Anthropic: Use anthropic/claude-sonnet-4-6 [2]. Azure AI Foundry: Use azure_ai/claude-sonnet-4-6 [2]. Google Vertex AI: Use vertex_ai/claude-sonnet-4-6 [2]. Amazon Bedrock: Use bedrock/anthropic.claude-sonnet-4-6-v1 [2]. LiteLLM has also implemented pattern-based matching for Claude model names, which allows it to automatically route new models following the claude-{family}-{major}-{minor}[-YYYYMMDD] naming convention to the Anthropic provider [5], simplifying integration for future releases.

Citations:


gemini-3-pro 기본값은 제거하거나 현재 LiteLLM 게이트웨이에 등록되어 있는 모델명으로 교체하세요.

claude-sonnet-4-6은 LiteLLM에서 지원하지만, gemini-3-pro는 Google에서 폐기된 모델 라벨입니다. LiteLLM 게이트웨이에 같은 별칭이 없으면 평가/포스트프로세스 단계가 Request failed with status 4xx로 발생하고 배치 degraded 결과로 이어집니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/defaults.ts` around lines 1 - 4, Update
DEFAULT_VALIDATION_MODEL in defaults.ts to remove the deprecated gemini-3-pro
label or replace it with a model name currently registered in the LiteLLM
gateway, while leaving DEFAULT_TRANSLATION_MODEL and DEFAULT_POSTPROCESS_MODEL
unchanged.

Comment thread scripts/translation-pipeline/src/report.ts
Comment on lines +107 to +158
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60_000);
let response: Response;
try {
response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
// TokenHub(사내 LLM 게이트웨이) 필수 집계 라벨. 일반 LiteLLM proxy는 무시한다.
'X-Client-Id': 'vapor-ui-translation-pipeline',
},
body: JSON.stringify({
model: options.model ?? 'claude-sonnet-4-6',
messages,
...(options.jsonSchema
? {
response_format: {
type: 'json_schema',
json_schema: {
name: options.jsonSchema.name,
strict: true,
schema: options.jsonSchema.schema,
},
},
}
: options.responseFormat === 'json'
? { response_format: { type: 'json_object' } }
: {}),
}),
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}

if (!response.ok) {
return {
content: null,
error: `Request failed with status ${response.status}`,
statusCode: response.status,
};
}

const data = (await response.json()) as {
choices?: { message?: { content?: unknown } }[];
} & Record<string, unknown>;
const raw = data.choices?.[0]?.message?.content;
if (typeof raw !== 'string') {
return { content: null, error: 'Unexpected response shape' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

타임아웃이 응답 본문 읽기를 보호하지 않습니다.

fetch는 응답 헤더가 도착하면 resolve합니다. 본문은 아직 스트리밍 중입니다. line 140-142의 finally가 이 시점에 clearTimeout(timeout)을 실행합니다. 그 다음 line 152의 await response.json()은 아무 타임아웃 없이 본문을 읽습니다.

게이트웨이가 헤더만 보내고 본문 전송을 멈추면 await response.json()은 무한히 대기합니다. AbortController는 이미 해제되었으므로 중단 수단이 없습니다. translator.tsforEachWithConcurrency 풀 안에서 이 호출을 await합니다. 따라서 정지된 호출 하나가 풀 슬롯을 영구히 점유하고, CLI 프로세스가 종료되지 않습니다.

본문을 모두 읽은 뒤에 타임아웃을 해제하세요.

🐛 제안 수정
     try {
         const controller = new AbortController();
         const timeout = setTimeout(() => controller.abort(), 60_000);
-        let response: Response;
         try {
-            response = await fetch(`${baseUrl}/chat/completions`, {
+            const response = await fetch(`${baseUrl}/chat/completions`, {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json',
                     Authorization: `Bearer ${apiKey}`,
                     // TokenHub(사내 LLM 게이트웨이) 필수 집계 라벨. 일반 LiteLLM proxy는 무시한다.
                     'X-Client-Id': 'vapor-ui-translation-pipeline',
                 },
                 body: JSON.stringify({
                     model: options.model ?? DEFAULT_TRANSLATION_MODEL,
                     messages,
                     ...(options.jsonSchema
                         ? {
                               response_format: {
                                   type: 'json_schema',
                                   json_schema: {
                                       name: options.jsonSchema.name,
                                       strict: true,
                                       schema: options.jsonSchema.schema,
                                   },
                               },
                           }
                         : options.responseFormat === 'json'
                           ? { response_format: { type: 'json_object' } }
                           : {}),
                 }),
                 signal: controller.signal,
             });
-        } finally {
-            clearTimeout(timeout);
-        }
 
-        if (!response.ok) {
-            return {
-                content: null,
-                error: `Request failed with status ${response.status}`,
-                statusCode: response.status,
-            };
-        }
+            if (!response.ok) {
+                return {
+                    content: null,
+                    error: `Request failed with status ${response.status}`,
+                    statusCode: response.status,
+                };
+            }
 
-        const data = (await response.json()) as {
-            choices?: { message?: { content?: unknown } }[];
-        } & Record<string, unknown>;
+            const data = (await response.json()) as {
+                choices?: { message?: { content?: unknown } }[];
+            } & Record<string, unknown>;
+            return buildResult(response, data, options);
+        } finally {
+            clearTimeout(timeout);
+        }
     } catch (error) {

buildResult는 기존 line 160-168의 반환 객체를 그대로 옮긴 헬퍼입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translation/client.ts` around lines 107 -
158, Update the timeout lifecycle in the translation client’s fetch flow so the
AbortController remains active through await response.json(), and only clear the
timeout after the response body has been fully read. Preserve the existing
response validation and result construction, optionally extracting the current
return object into the suggested buildResult helper without changing its
behavior.

Comment on lines +105 to +128
function validateBatchEvaluations(units: TranslationUnit[], evaluations: unknown): BatchMqmResult {
if (!Array.isArray(evaluations)) {
return invalidMqm('MQM batch response must contain evaluations[]');
}

try {
const items = reconcileById(
units.map(getTranslationUnitKey),
evaluations as BatchEvaluationItem[],
);
return {
ok: true,
evaluations: new Map(
[...items].map(([id, item]) => [
id,
{ verdict: item.verdict, errors: item.errors },
]),
),
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return invalidMqm(message);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

LLM 응답의 verdicterrors를 런타임에서 검증하지 않습니다. isMqmError가 사용되지 않습니다.

line 113은 evaluations as BatchEvaluationItem[]으로 캐스팅만 합니다. reconcileByIditem.id만 확인합니다. item.verdictitem.errors는 검사 없이 line 120에서 MqmResult로 들어갑니다.

validator.ts는 이 목적의 isMqmError를 export합니다. 이 파일은 line 13-17에서 MQM_CATEGORY_VALUES, MQM_EVALUATOR_PROMPT, MQM_SEVERITY_VALUES만 가져오고 isMqmError는 호출하지 않습니다.

strict: true JSON 스키마는 게이트웨이가 네이티브 structured output을 지원할 때만 강제됩니다. LiteLLM은 미지원 모델에서 프롬프트 기반 JSON으로 대체합니다. DEFAULT_VALIDATION_MODEL이 그런 모델이면 응답 형태가 보장되지 않습니다.

결과는 두 가지입니다. verdict'PASS' 외의 값이면 통과한 유닛이 불필요하게 후편집 경로로 들어가 비용과 지연이 늘어납니다. errors가 배열이 아니면 MqmError[]로 선언된 TranslationOutcome.errors에 비배열이 저장되고, 이를 순회하는 리포트 생성 코드가 실패합니다.

🐛 제안 수정
 import {
     MQM_CATEGORY_VALUES,
     MQM_EVALUATOR_PROMPT,
     MQM_SEVERITY_VALUES,
+    isMqmError,
 } from '~/validation/validator';
+function toEvaluation(id: string, item: BatchEvaluationItem): MqmResult {
+    if (item.verdict !== 'PASS' && item.verdict !== 'FAIL') {
+        throw new Error(`Invalid verdict for id: ${id}`);
+    }
+    if (!Array.isArray(item.errors) || !item.errors.every(isMqmError)) {
+        throw new Error(`Invalid errors[] for id: ${id}`);
+    }
+    return { verdict: item.verdict, errors: item.errors };
+}
+
 function validateBatchEvaluations(units: TranslationUnit[], evaluations: unknown): BatchMqmResult {
     if (!Array.isArray(evaluations)) {
         return invalidMqm('MQM batch response must contain evaluations[]');
     }
 
     try {
         const items = reconcileById(
             units.map(getTranslationUnitKey),
             evaluations as BatchEvaluationItem[],
         );
         return {
             ok: true,
-            evaluations: new Map(
-                [...items].map(([id, item]) => [
-                    id,
-                    { verdict: item.verdict, errors: item.errors },
-                ]),
-            ),
+            evaluations: new Map([...items].map(([id, item]) => [id, toEvaluation(id, item)])),
         };
     } catch (error) {

toEvaluation이 던지는 오류는 기존 catch가 받아 invalidMqm으로 변환합니다. 따라서 해당 배치는 batch_mqm_failed로 degrade되며, 이는 가이드라인이 요구하는 동작입니다.

가이드라인에 따라: "Malformed MQM, postprocess, or final MQM responses must mark affected units unverified with the corresponding reason."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function validateBatchEvaluations(units: TranslationUnit[], evaluations: unknown): BatchMqmResult {
if (!Array.isArray(evaluations)) {
return invalidMqm('MQM batch response must contain evaluations[]');
}
try {
const items = reconcileById(
units.map(getTranslationUnitKey),
evaluations as BatchEvaluationItem[],
);
return {
ok: true,
evaluations: new Map(
[...items].map(([id, item]) => [
id,
{ verdict: item.verdict, errors: item.errors },
]),
),
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return invalidMqm(message);
}
}
function toEvaluation(id: string, item: BatchEvaluationItem): MqmResult {
if (item.verdict !== 'PASS' && item.verdict !== 'FAIL') {
throw new Error(`Invalid verdict for id: ${id}`);
}
if (!Array.isArray(item.errors) || !item.errors.every(isMqmError)) {
throw new Error(`Invalid errors[] for id: ${id}`);
}
return { verdict: item.verdict, errors: item.errors };
}
function validateBatchEvaluations(units: TranslationUnit[], evaluations: unknown): BatchMqmResult {
if (!Array.isArray(evaluations)) {
return invalidMqm('MQM batch response must contain evaluations[]');
}
try {
const items = reconcileById(
units.map(getTranslationUnitKey),
evaluations as BatchEvaluationItem[],
);
return {
ok: true,
evaluations: new Map([...items].map(([id, item]) => [id, toEvaluation(id, item)])),
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return invalidMqm(message);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts` around lines
105 - 128, Update validateBatchEvaluations to validate each reconciled
evaluation’s verdict and errors using the exported isMqmError from validator.ts
before constructing the MqmResult map; reject malformed values through the
existing catch path so the batch degrades to invalidMqm and affected units
remain unverified with the corresponding reason.

Source: Coding guidelines

Comment on lines +1 to +7
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import * as cacheModule from '~/cache/cache';
import * as clientModule from '~/translation/client';
import * as translationModule from '~/translation/translate';
import { translatePropsInfo } from '~/translator/translator';
import { type TranslatableDoc, getTranslationUnitKey } from '~/types';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

vi.mock('~/translation/client')으로 모듈 전체를 모킹하세요.

이 파일은 vi.spyOn(clientModule, 'callLlm')만 사용합니다. 코딩 가이드라인은 LLM 호출을 vi.mock('~/translation/client')으로 모킹하도록 요구합니다.

vi.spyOn은 테스트마다 스파이를 설치해야 동작합니다. line 63-65의 afterEachvi.restoreAllMocks()로 실제 구현을 복원합니다. 스파이를 설치하지 않는 테스트를 새로 추가하면 실제 callLlm이 실행되고 LITELLM_BASE_URL로 네트워크 요청을 보냅니다. 모듈 수준 vi.mock은 이 경로를 원천 차단합니다.

♻️ 제안 수정
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
 import * as cacheModule from '~/cache/cache';
 import * as clientModule from '~/translation/client';
 import * as translationModule from '~/translation/translate';
 import { translatePropsInfo } from '~/translator/translator';
 import { type TranslatableDoc, getTranslationUnitKey } from '~/types';
 
+vi.mock('~/translation/client', () => ({
+    callLlm: vi.fn(async () => {
+        throw new Error('callLlm was not mocked in this test');
+    }),
+}));
+
 const sampleProps: TranslatableDoc[] = [

기존 vi.spyOn(clientModule, 'callLlm') 호출은 그대로 두어도 모킹된 모듈 위에서 동작합니다.

가이드라인에 따라: "Mock all LLM calls with vi.mock('~/translation/client'); tests must not call a real API."

Also applies to: 32-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/translator.test.ts` around lines
1 - 7, 모듈 수준에서 translation client 전체를 모킹하도록 설정하세요. `translator.test.ts`의 import
영역 인근에 `vi.mock('~/translation/client')`를 추가해 모든 테스트에서 실제 `callLlm` 구현과 네트워크 요청이
실행되지 않게 하되, 기존 `vi.spyOn(clientModule, 'callLlm')` 호출은 모킹된 모듈 위에서 계속 사용하세요.

Source: Coding guidelines

Comment on lines +21 to +30
function mockTranslations(translations: Record<string, string>): void {
vi.spyOn(translationModule, 'translateUnits').mockImplementation(async (units) => {
return new Map(
units.map((unit) => [
getTranslationUnitKey(unit),
translations[unit.id] ?? unit.source,
]),
);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

mockTranslations의 조회 키를 getTranslationUnitKey(unit)로 바꾸세요.

line 25는 반환 Map을 getTranslationUnitKey(unit)로 올바르게 키잉합니다. line 26은 fixture 조회에 bare unit.id를 사용합니다.

unit.id는 컴포넌트 안에서만 유일합니다. 지금은 sampleProps에서 Button만 description을 가지므로 문제가 없습니다. 두 컴포넌트가 모두 component.description을 가지는 테스트를 추가하면, 두 유닛이 같은 fixture 값을 받습니다. 그 결과는 중복 제거 버그처럼 보이지만 원인은 이 헬퍼입니다.

♻️ 제안 수정
 function mockTranslations(translations: Record<string, string>): void {
     vi.spyOn(translationModule, 'translateUnits').mockImplementation(async (units) => {
         return new Map(
             units.map((unit) => [
                 getTranslationUnitKey(unit),
-                translations[unit.id] ?? unit.source,
+                translations[getTranslationUnitKey(unit)] ??
+                    translations[unit.id] ??
+                    unit.source,
             ]),
         );
     });
 }

호출부는 '0:component.description' 형태의 복합 키로 점진 전환할 수 있습니다.

가이드라인에 따라: "Batch and request/response reconciliation must use getTranslationUnitKey(unit) (${componentIndex}:${id}), never the bare unit.id, because IDs are only unique within a component."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function mockTranslations(translations: Record<string, string>): void {
vi.spyOn(translationModule, 'translateUnits').mockImplementation(async (units) => {
return new Map(
units.map((unit) => [
getTranslationUnitKey(unit),
translations[unit.id] ?? unit.source,
]),
);
});
}
function mockTranslations(translations: Record<string, string>): void {
vi.spyOn(translationModule, 'translateUnits').mockImplementation(async (units) => {
return new Map(
units.map((unit) => [
getTranslationUnitKey(unit),
translations[getTranslationUnitKey(unit)] ??
translations[unit.id] ??
unit.source,
]),
);
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/translator.test.ts` around lines
21 - 30, Update mockTranslations to use getTranslationUnitKey(unit) for the
translations fixture lookup, matching the composite key used when constructing
the returned Map. Remove the bare unit.id lookup so units with identical IDs in
different components resolve independently.

Source: Coding guidelines

Comment thread scripts/translation-pipeline/src/translator.ts
Comment on lines +12 to +15
// ponytail: 험프 2개 이상만 식별자로 본다. `Button`처럼 험프 하나인 컴포넌트명은
// 평범한 영어 단어(Whether, This)와 구별할 수 없어 일부러 흘려보낸다.
// 오탐이 문제되면 여기서 실제 컴포넌트·prop 이름 사전을 받도록 바꿀 것.
const MULTI_HUMP_IDENTIFIER = /\b[A-Za-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+\b/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

단일 hump 및 하이픈 식별자를 결정론적으로 검사하십시오.

Line 15는 onClick은 검사하지만 Button, size, aria-label, data-state는 검사하지 않습니다. 이 값이 원문에 일반 텍스트로 있으면 번역 또는 변형되어도 checkPreservation이 위반을 반환하지 않습니다.

TranslationUnitcomponentName 및 prop 이름을 호출부에서 전달하십시오. 원문에 실제로 나타난 알려진 식별자와 hyphenated HTML/ARIA 식별자를 정확히 보존 검사하십시오. 관련 테스트도 추가하십시오. 그렇지 않으면 batch-lifecycle.ts가 변형된 API 문서를 정상 번역으로 처리할 수 있습니다.

As per coding guidelines, validation/preserve.ts must contain deterministic preservation checks for bare identifiers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/validation/preserve.ts` around lines 12 -
15, Update the preservation validation around MULTI_HUMP_IDENTIFIER and
checkPreservation to accept TranslationUnit.componentName and prop names from
callers, then deterministically preserve known bare identifiers and hyphenated
HTML/ARIA identifiers such as Button, size, aria-label, and data-state when they
appear in the source. Retain the existing multi-hump behavior while adding
coverage for single-hump and hyphenated cases, including transformed API
documentation.

Source: Coding guidelines

… helpers

- util.ts 신설 — chunkArray·reconcileById 3중 중복 제거
- types.ts makeOutcome — assurance/reportable을 reason에서 파생
- batch-lifecycle.ts callBatch 공통화 (MQM·후편집 중복 제거)
- isMqmError·MqmResult.unavailable·CliError·client.ts usage/responseCost 제거 (소비처 0건)
- report.ts outcomes.get(unit.id) 폴백 제거 — 키는 getTranslationUnitKey 하나
추출기의 findExportedInterfaceProps는 이름과 달리 type alias만 본다. `export interface Props`로
쓰인 SheetRoot·SheetResizeHandle이 경고 한 줄 없이 버려져 sheet.mdx의 표 두 개가 에러를 렌더했다.

- sheet.tsx — 두 namespace의 Props를 type alias로 통일 (F1)
- parse.ts — exported `interface Props`를 발견하고 버릴 때 경고 (J1). 같은 함정 재발 방지용
- package.json — `i18n`이 두 CLI를 먼저 빌드한다. dist/가 gitignore라 새 클론에서 깨졌다 (F4)
- run.ts — ko 파일도 prettier를 지나게 한다. en과 짧은 배열 포맷이 갈렸다 (F6)
- defaults.ts — 상대 경로 기준 cwd 주석을 실제 호출 지점(apps/website)에 맞게 고침 (F10)

run.ts에는 앞 커밋의 CliError 제거가 함께 담겼다 — 같은 파일이라 분리하지 않았다.

생성 JSON은 이 커밋에 없다. sheet-root·sheet-resize-handle 표는 재추출을 커밋하는
후속 브랜치에서 사이트에 뜬다.
MDX 절 제목을 kebab-case로 바꿔 componentName과 대조하니 7건이 어긋났다. 깨진 참조는 에러가 보여
언젠가 잡히지만, 오매핑은 남의 표를 조용히 렌더해 사용자가 없는 API를 믿게 만든다.

- floating-bar.mdx — portal·positioner·popup 세 참조에 `-primitive` 접미사가 빠져 있었다.
  이 중 popup은 FloatingBar.Popup 표를 두 번 렌더하고 있었다
- menu.mdx — 이쪽 3건은 componentName이 아니라 절 제목이 틀렸다. 하위 `####` 절에
  PositionerPrimitive·CheckboxItemIndicatorPrimitive·RadioItemIndicatorPrimitive가 이미
  올바르게 있어서, 상위 `###` 제목이 각각 Menu.Popup·Menu.Item·Menu.Separator여야 한다.
  componentName을 고쳤다면 표가 중복되고 공개 파트 셋의 문서가 사라졌을 것이다
- toast.mdx — `### useToastManager` 절 삭제. toast-object는 namespace가 아닌 내부 type이라
  추출기가 원리적으로 만들 수 없고, 지금 렌더되는 것은 에러 문자열이라 정보 손실이 없다

수정 후 같은 대조를 다시 돌려 어긋남 0건을 확인했다.
번역 생성물은 별도 PR에서 반영한다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/translation-pipeline/src/translator/batch-lifecycle.ts (1)

117-153: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

LLM 응답의 verdicterrors를 런타임에서 검증하지 않습니다.

Line 144-152는 callBatch가 반환한 항목을 검증 없이 { verdict: item.verdict, errors: item.errors }로 그대로 사용합니다. reconcileByIditem.id만 확인합니다. validator.ts는 이 목적의 isMqmError를 export하지만 호출하지 않습니다.

strict: true JSON 스키마는 게이트웨이가 네이티브 structured output을 지원하는 모델에서만 강제됩니다. DEFAULT_VALIDATION_MODEL이 프롬프트 기반 JSON으로 대체되면 응답 형태가 보장되지 않습니다. verdict'PASS'/'FAIL'이 아니거나 errors가 배열이 아니면, 잘못된 값이 MqmResult로 저장됩니다. 이는 이후 리포트 생성 시 errors를 순회하는 코드를 실패시킬 수 있습니다.

또한 translator.ts는 MQM 배치 처리(processBatchLifecycle 호출)를 번역 배치와 달리 try/catch로 감싸지 않습니다. 캐시는 MQM 단계 종료 후 한 번만 저장되므로, 여기서 예외가 전파되면 전체 실행이 중단되고 그때까지 누적된 캐시가 저장되지 않을 위험이 있습니다.

isMqmError로 각 항목을 검증하고, 실패 시 예외를 던져 기존 catch 경로가 배치를 무효 결과(batch_mqm_failed 등)로 격하시키도록 하십시오.

🐛 제안 수정
 import {
     MQM_CATEGORY_VALUES,
     MQM_EVALUATOR_PROMPT,
     MQM_SEVERITY_VALUES,
+    isMqmError,
 } from '~/validation/validator';
+function toMqmResult(id: string, item: BatchEvaluationItem): MqmResult {
+    if (item.verdict !== 'PASS' && item.verdict !== 'FAIL') {
+        throw new Error(`Invalid verdict for id: ${id}`);
+    }
+    if (!Array.isArray(item.errors) || !item.errors.every(isMqmError)) {
+        throw new Error(`Invalid errors[] for id: ${id}`);
+    }
+    return { verdict: item.verdict, errors: item.errors };
+}
+
     if (!result.ok) return result;

     return {
         ok: true,
-        value: new Map(
-            [...result.value].map(([id, item]) => [
-                id,
-                { verdict: item.verdict, errors: item.errors },
-            ]),
-        ),
+        value: new Map([...result.value].map(([id, item]) => [id, toMqmResult(id, item)])),
     };

As per coding guidelines: "Malformed MQM, postprocess, or final-MQM responses must mark affected units unverified with the corresponding failure reason."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts` around lines
117 - 153, Validate every MQM result in validateBatchWithMqm using the exported
isMqmError before constructing MqmResult, and reject malformed verdict or errors
values instead of storing them. Ensure the processBatchLifecycle MQM path in
translator.ts catches that failure, marks the affected units unverified with the
corresponding batch_mqm_failed reason, and allows accumulated cache state to be
persisted.

Source: Coding guidelines

scripts/translation-pipeline/src/cli/run.ts (1)

188-223: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

npx prettier가 아니라 로컬 prettier 명령어를 사용하십시오.

scripts/translation-pipeline/package.jsonprettier가 선언되어 있어 현재는 실행을 막지 않습니다. 하지만 execFileSync('npx', ['prettier', ...])는 외부 npx에 의존하므로 이 CLI의 실행 의존성 모델과 맞지 않습니다. root package의 prettier: ^3.8.3 의존성을 유지해야 하므로, 해당 하위 프로젝트에서 직접 실행할 수 없는 로컬 명령어를 사용하는 방식은 변경하십시오.

execFileSync('npx', ['prettier', '--write', ...filePaths], { stdio: 'inherit' })prettier 명령어가 프로젝트 내에서 직접 실행되도록 교체하고, 네트워크 잠금·타임아웃 문제는 제거한 뒤 실패는 preservation_fallback/format_failed 형태로 보고하도록 처리하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/cli/run.ts` around lines 188 - 223, Update
formatWithPrettier to invoke the project-local prettier executable directly
instead of npx, preserving the existing --write behavior and inherited stdio.
Remove reliance on external network resolution or npx timeouts, and propagate
formatting failures through the run/report flow as preservation_fallback or
format_failed rather than silently treating them as a warning.
♻️ Duplicate comments (1)
scripts/translation-pipeline/src/translation/client.ts (1)

58-109: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

타임아웃이 응답 본문 읽기를 보호하지 않습니다.

fetch는 응답 헤더가 도착하면 resolve됩니다. 본문은 아직 스트리밍 중입니다. Line 89-91의 finally가 이 시점에 clearTimeout(timeout)을 실행합니다. Line 101의 await response.json()은 이후 타임아웃 보호 없이 본문을 읽습니다.

게이트웨이가 헤더만 보내고 본문 전송을 멈추면 await response.json()은 무한히 대기합니다. AbortController는 이미 해제되었으므로 중단 수단이 없습니다. translator.tsforEachWithConcurrency 풀 안에서 이 호출을 await합니다. 따라서 정지된 호출 하나가 풀 슬롯을 영구히 점유하고, CLI 프로세스가 종료되지 않습니다.

본문을 모두 읽은 뒤에 타임아웃을 해제하십시오.

🐛 제안 수정
     try {
         const controller = new AbortController();
         const timeout = setTimeout(() => controller.abort(), 60_000);
-        let response: Response;
         try {
-            response = await fetch(`${baseUrl}/chat/completions`, {
+            const response = await fetch(`${baseUrl}/chat/completions`, {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json',
                     Authorization: `Bearer ${apiKey}`,
                     'X-Client-Id': 'vapor-ui-translation-pipeline',
                 },
                 body: JSON.stringify({
                     model: options.model ?? 'claude-sonnet-4-6',
                     messages,
                     ...(options.jsonSchema
                         ? {
                               response_format: {
                                   type: 'json_schema',
                                   json_schema: {
                                       name: options.jsonSchema.name,
                                       strict: true,
                                       schema: options.jsonSchema.schema,
                                   },
                               },
                           }
                         : {}),
                 }),
                 signal: controller.signal,
             });
-        } finally {
-            clearTimeout(timeout);
-        }
 
-        if (!response.ok) {
-            return {
-                content: null,
-                error: `Request failed with status ${response.status}`,
-                statusCode: response.status,
-            };
-        }
+            if (!response.ok) {
+                return {
+                    content: null,
+                    error: `Request failed with status ${response.status}`,
+                    statusCode: response.status,
+                };
+            }
 
-        const data = (await response.json()) as {
-            choices?: { message?: { content?: unknown } }[];
-        };
-        const raw = data.choices?.[0]?.message?.content;
-        if (typeof raw !== 'string') {
-            return { content: null, error: 'Unexpected response shape' };
-        }
-
-        return { content: raw };
+            const data = (await response.json()) as {
+                choices?: { message?: { content?: unknown } }[];
+            };
+            const raw = data.choices?.[0]?.message?.content;
+            if (typeof raw !== 'string') {
+                return { content: null, error: 'Unexpected response shape' };
+            }
+            return { content: raw };
+        } finally {
+            clearTimeout(timeout);
+        }
     } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translation-pipeline/src/translation/client.ts` around lines 58 -
109, Update the timeout lifecycle in the request flow around the inner fetch
try/finally and response.json so the timer remains active while the response
body is being read. Move clearTimeout(timeout) to execute only after
response.json completes (including failures), preserving abort behavior for
stalled bodies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/ts-api-extractor/src/config/defaults.ts`:
- Around line 7-10: Align the documentation in the defaults configuration
comments with the actual path resolution used for inputPath, tsconfig, and
outputDir: either change resolution to use the explicit configuration file’s
directory or revise the “relative to that file” guidance to state that paths
resolve from process.cwd().

---

Outside diff comments:
In `@scripts/translation-pipeline/src/cli/run.ts`:
- Around line 188-223: Update formatWithPrettier to invoke the project-local
prettier executable directly instead of npx, preserving the existing --write
behavior and inherited stdio. Remove reliance on external network resolution or
npx timeouts, and propagate formatting failures through the run/report flow as
preservation_fallback or format_failed rather than silently treating them as a
warning.

In `@scripts/translation-pipeline/src/translator/batch-lifecycle.ts`:
- Around line 117-153: Validate every MQM result in validateBatchWithMqm using
the exported isMqmError before constructing MqmResult, and reject malformed
verdict or errors values instead of storing them. Ensure the
processBatchLifecycle MQM path in translator.ts catches that failure, marks the
affected units unverified with the corresponding batch_mqm_failed reason, and
allows accumulated cache state to be persisted.

---

Duplicate comments:
In `@scripts/translation-pipeline/src/translation/client.ts`:
- Around line 58-109: Update the timeout lifecycle in the request flow around
the inner fetch try/finally and response.json so the timer remains active while
the response body is being read. Move clearTimeout(timeout) to execute only
after response.json completes (including failures), preserving abort behavior
for stalled bodies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a47f48ea-6cb8-4074-b99a-61724bd9028f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d1daa7 and 22f7fc4.

📒 Files selected for processing (22)
  • .changeset/lazy-sheets-align.md
  • apps/website/content/docs/components/(components)/floating-bar.mdx
  • apps/website/content/docs/components/(components)/menu.mdx
  • apps/website/content/docs/components/(components)/toast.mdx
  • apps/website/package.json
  • packages/core/src/components/sheet/sheet.tsx
  • scripts/translation-pipeline/CLAUDE.md
  • scripts/translation-pipeline/src/cli/index.ts
  • scripts/translation-pipeline/src/cli/run.test.ts
  • scripts/translation-pipeline/src/cli/run.ts
  • scripts/translation-pipeline/src/report/report.ts
  • scripts/translation-pipeline/src/translation/client.test.ts
  • scripts/translation-pipeline/src/translation/client.ts
  • scripts/translation-pipeline/src/translation/translate.test.ts
  • scripts/translation-pipeline/src/translation/translate.ts
  • scripts/translation-pipeline/src/translator/batch-lifecycle.ts
  • scripts/translation-pipeline/src/translator/translator.ts
  • scripts/translation-pipeline/src/types.ts
  • scripts/translation-pipeline/src/util.ts
  • scripts/translation-pipeline/src/validation/validator.ts
  • scripts/ts-api-extractor/src/config/defaults.ts
  • scripts/ts-api-extractor/src/stages/parse.ts
💤 Files with no reviewable changes (2)
  • scripts/translation-pipeline/src/validation/validator.ts
  • apps/website/content/docs/components/(components)/toast.mdx
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/website/package.json
  • scripts/translation-pipeline/src/cli/run.test.ts
  • scripts/translation-pipeline/src/translation/translate.test.ts
  • scripts/translation-pipeline/CLAUDE.md
  • scripts/translation-pipeline/src/cli/index.ts

Comment on lines +7 to +10
* `process.cwd()`. These defaults assume the tool runs from `apps/website`
* (that is how `pnpm --filter website extract` invokes it). For other
* invocation contexts, provide an explicit config file
* (e.g. docs-extractor.config.mjs) with paths relative to that file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'process\.cwd|config.*dir|inputPath|tsconfig|outputDir' scripts/ts-api-extractor/src

Repository: goorm-dev/vapor-ui

Length of output: 13958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'defaults.ts:\n'
cat -n scripts/ts-api-extractor/src/config/defaults.ts
printf '\nloader.ts relevant section:\n'
sed -n '1,90p' scripts/ts-api-extractor/src/config/loader.ts | cat -n
printf '\noptions.ts relevant section:\n'
sed -n '1,95p' scripts/ts-api-extractor/src/cli/options.ts | cat -n
printf '\nextract.ts relevant section:\n'
sed -n '1,25p' scripts/ts-api-extractor/src/extract.ts | cat -n

Repository: goorm-dev/vapor-ui

Length of output: 8203


설정 파일 경로의 기준을 실제 해석 기준과 일치시키세요.

사용자 설정의 inputPath, tsconfig, outputDir는 모두 path.resolve(process.cwd(), ...)로 해석됩니다. 그러나 문서가 명시적 설정 파일에 대해서는 경로 기준이 “그 파일 기준”이라고 안내하므로 설정 파일 작성가가 잘못된 상대 경로를 사용합니다. 경로 해석을 설정 파일 디렉토리 기준으로 바꾸거나, 현재 문서의 “file relative” 문구를 process.cwd() 기준으로 수정하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ts-api-extractor/src/config/defaults.ts` around lines 7 - 10, Align
the documentation in the defaults configuration comments with the actual path
resolution used for inputPath, tsconfig, and outputDir: either change resolution
to use the explicit configuration file’s directory or revise the “relative to
that file” guidance to state that paths resolve from process.cwd().

…ng it

LOCALE_PREFERENCE 상수와 헬퍼 함수로 폴백 순서를 코드에 드러냈다.
사내 게이트웨이 이름·내부 URL·비공개 가이드 링크를 예시 파일과 주석에서 빼고, OpenAI 호환 엔드포인트라는 사실만 남겼다.
설정 파일 로딩 전체를 걷어냈다. `docs-extractor.config.*`는 저장소에 하나도 없었고
`--config`·`--no-config`는 meow flags에 파싱조차 되지 않아 defaults.ts만 쓰이고 있었다.
설정이 필요하면 `extractorConfig`를 직접 고친다.

같은 이유로 `verbose`와 `all`도 지웠다. 둘 다 CLI 플래그가 없어 항상 false였으므로
logResolution, ParseConfig, resolveType의 4번째 인자, extract.ts의 삼항 3개가 모두
죽은 분기였다. 컴포넌트별 `include` 설정도 소비처와 함께 사라졌고, 같은 일을 하는
`includeHtml`이 남는다.

declaration-source.ts는 실제로 호출되는 classifyPropSource 하나만 남겼다. 나머지
헬퍼 7개는 테스트에서만 쓰였고, 경로 매칭 자체는 classifyPropSource가 이미 한다.

라이브러리 엔트리(src/index.ts, package.json의 main/exports, tsup LIB 빌드)도 뺐다.
private 패키지라 import하는 곳이 없고 bin 하나만 쓰인다.

README는 실제 코드에 맞췄다 — 존재한 적 없는 `languages` 옵션, 파싱되지 않던 플래그 4개,
출력에 없는 `displayName`, stages/ 도입 전의 파일 배치를 정리했다.

리팩토링 전후로 extract 산출물 218개가 바이트 단위로 동일한 것을 확인했다.
…xplicit model

캐시 값에서 `source`를 뺐다. 키가 원문 해시라 이 필드를 읽는 곳이 없었고,
저장 형태는 `Map<string, string>`이면 충분하다.

`callLlm`의 `model` 기본값도 지웠다. 호출자 세 곳이 전부 모델을 명시로 넘기고 있어
defaults.ts의 모델명과 문자열만 중복됐고, 한쪽만 고치면 조용히 갈라지는 자리였다.
이제 `model`은 필수 인자다.
…r unwrapping

`errorMessage()` 헬퍼 하나로 여덟 곳의 `error instanceof Error` 삼항을 걷어냈다.
소비자가 원본에서 직접 가져가는 재수출 2개와 사용처 0인 `export` 3개를 지우고,
호출부가 이미 검사하는 `if (!outputDir) return` 가드 3개도 함께 없앴다.
`preserve.ts`의 `matchAll` 래퍼는 호출 네 곳에 인라인했다.
`batch-call.ts`가 "스키마 콜 → content 확인 → JSON 파싱 → id 대조"를 한 곳에서 맡는다.
`translate.ts`와 `batch-lifecycle.ts`에 각각 있던 껍데기 2벌이 사라지고,
소비자가 하나뿐이던 `json.ts`와 `util.ts`의 `reconcileById`가 그 안으로 흡수됐다.
`translate.ts`는 여전히 실패를 던진다 — `translator.ts`의 catch가 배치를 영어 폴백으로 격하시켜야 한다.
…tage layout

디렉토리 6개를 없애고 단계 하나가 파일 하나로 서게 했다. `cli/run.ts`가 겸했던 입력·출력은
`input.ts`·`output.ts`로 갈라졌고, `run.ts`에는 호출 순서만 남았다. 406줄이던
`batch-lifecycle.ts`는 `batch/`의 lifecycle·mqm·postprocess 세 파일로 쪼갰다 —
재검사는 `mqm.ts`·`preserve.ts` 재호출이라 새 코드가 없어 lifecycle의 흐름으로 남겼다.
`types.ts`는 이름이 내용을 속이던 문제를 `domain.ts` 개명으로 풀었고, `validator.ts`는
소비자가 하나뿐이라 `batch/mqm.ts`로 흡수했다. 테스트용 DI 구멍(`RunOptions`)은
`vi.stubEnv`·`vi.spyOn`으로 대체해 프로덕션 인터페이스에서 지웠다.

E2E 경로의 산출물(`ko/*.json`·`.i18n-report.md`)을 리팩토링 전후로 덤프해 대조했고 차이가 없다.
게이트웨이가 인증을 요구하지 않아 키가 실제로 쓰인 적이 없다.
`.env.example`이 `unused`를 채워 두라고 안내하던 것이 그 증거다.
헤더를 옵셔널로 남기는 분기도 넣을 계획이 없으니 함께 지웠다 —
필요해지면 한 줄이다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants