diff --git a/.github/workflows/auto-translate.yml b/.github/workflows/auto-translate.yml
new file mode 100644
index 0000000..f9d537e
--- /dev/null
+++ b/.github/workflows/auto-translate.yml
@@ -0,0 +1,68 @@
+name: Auto Translate (Push to New Branch)
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+jobs:
+ translate:
+ if: "!contains(github.ref_name, 'auto-translate')"
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write
+
+ steps:
+ # 1️⃣ Checkout source branch
+ - name: Checkout source branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.ref_name }}
+
+ # 2️⃣ Setup Node environment
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ # 3️⃣ Install dependencies
+ - name: Install deps
+ run: |
+ npm init -y
+ npm install openai fs-extra
+
+ # 4️⃣ Run translation (generate cn/ ko/ files)
+ - name: Run translation
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ run: node translate.js
+ timeout-minutes: 60
+
+ # 5️⃣ Create translation branch with timestamp and push
+ - name: Push translated files
+ run: |
+ # Timestamp (UTC, for stability)
+ TS=$(date -u '+%Y%m%d-%H%M%S')
+
+ SRC_BRANCH="${GITHUB_REF_NAME}"
+ TRANS_BRANCH="${SRC_BRANCH}-auto-translate-${TS}"
+
+ git config user.name "github-actions"
+ git config user.email "github-actions@github.com"
+
+ # Create new translation branch (new each time)
+ git checkout -b "$TRANS_BRANCH"
+
+ # Ensure directories exist (prevent glob errors)
+ mkdir -p cn/changelog
+ mkdir -p ko/changelog
+
+ # Only add translated files
+ git add cn/** ko/**
+
+ # Skip commit if no changes
+ git diff --cached --quiet || git commit -m "chore: auto translate (${TS})"
+
+ # Push new branch
+ git push origin "$TRANS_BRANCH"
\ No newline at end of file
diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md
new file mode 100644
index 0000000..4a0d734
--- /dev/null
+++ b/PR_DESCRIPTION.md
@@ -0,0 +1,109 @@
+# 添加自动翻译系统并完善多语言 Changelog 支持
+
+## 📋 概述
+
+本次 PR 引入了自动翻译工作流系统,为文档添加了韩文支持,并全面改进了中文和韩文 changelog 的翻译质量和本地化体验。
+
+## ✨ 主要功能
+
+### 1. 自动翻译系统
+- **新增 GitHub Actions 工作流** (`.github/workflows/auto-translate.yml`)
+ - 当推送到非 main 分支时自动触发翻译
+ - 自动创建翻译分支并提交翻译结果
+- **新增翻译脚本** (`translate.js`)
+ - 使用 OpenAI API 进行智能翻译
+ - 支持中文和韩文翻译
+ - 包含重试机制和分块处理
+
+### 2. 翻译质量改进
+- **专有名词智能识别**
+ - 自动识别首字母大写的专有名词(产品名、模块名等)
+ - 固定术语规则:`Frontier`、`Crypto Frontier`、`Robotics Frontier`、`Model Comparison` 等保持英文
+ - 特定术语统一翻译:`Lineage` → `血缘`,`How` → `运作方式`,`Timeline` → `活动时间`,`Access` → `参与方式`,`Lock` → `锁仓`
+- **自然语言表达**
+ - 避免直译,根据上下文用符合语言习惯的方式表达
+ - 例如:`action` 根据上下文翻译为 `操作`、`动作` 等,而非直译为 `行动`
+- **日期格式统一**
+ - 中文:`2025 年 12 月 04 日`(汉字和数字之间保留空格,月份和日期补零)
+ - 韩文:`2025년 12월 04일`(月份和日期补零)
+
+### 3. 多语言 Changelog 支持
+- **新增韩文 changelog** (`ko/changelog/2025.mdx`)
+ - 完整的 2025 年 changelog 韩文翻译
+ - 包含所有 UI 元素的本地化
+- **更新中文 changelog** (`cn/changelog/2025.mdx`)
+ - 改进翻译质量
+ - 统一日期格式
+ - 修复专有名词翻译
+- **更新英文 changelog** (`en/changelog/2025.mdx`)
+ - 添加 React hooks 导入以支持交互功能
+
+### 4. UI 元素本地化
+- **Front matter(元数据)**
+ - 中文:`title: "变更日志"`,`description: "本文档记录了 Codatta 在 2025 年的所有更新、修复和新功能。"`
+ - 韩文:`title: "변경 로그"`,`description: "이 변경 로그는 2025년 Codatta의 모든 업데이트, 수정 및 새로운 기능을 문서화합니다."`
+- **结果文本**
+ - 中文:`条结果`
+ - 韩文:`개 결과`
+- **过滤器标签**
+ - 中文:全部、核心功能发布、调整与优化、修复与功能下线、活动启动
+ - 韩文:전체、핵심 기능 출시、조정 및 최적화、수정 및 기능 종료、캠페인 시작
+- **月份标签**
+ - 中文:全部月份、十二月、十一月、十月等
+ - 韩文:전체、12월、11월、10월等
+
+### 5. 配置更新
+- **更新 `docs.json`**
+ - 添加韩文 changelog 导航配置
+
+## 🐛 修复的问题
+
+1. **解析错误修复**
+ - 修复韩文 changelog 中的代码块标记错误(` ```html` 和多余的 ` ````)
+ - 修复 JSX 解析错误
+
+2. **翻译一致性**
+ - 统一专有名词处理规则
+ - 统一日期格式
+ - 统一术语翻译
+
+3. **UI 元素翻译**
+ - 修复过滤器标签未翻译的问题
+ - 修复结果文本未翻译的问题
+ - 修复 front matter 未翻译的问题
+
+## 📁 文件变更
+
+- **新增文件:**
+ - `.github/workflows/auto-translate.yml` - 自动翻译工作流
+ - `translate.js` - 翻译脚本
+ - `ko/changelog/2025.mdx` - 韩文 changelog
+
+- **修改文件:**
+ - `cn/changelog/2025.mdx` - 中文 changelog 更新
+ - `en/changelog/2025.mdx` - 英文 changelog 更新
+ - `docs.json` - 导航配置更新
+
+## 🔄 工作流程
+
+1. 开发者推送代码到非 main 分支
+2. GitHub Actions 自动触发翻译工作流
+3. 工作流运行 `translate.js` 脚本
+4. 脚本读取英文 changelog,使用 OpenAI API 翻译为中文和韩文
+5. 自动创建翻译分支(格式:`{原分支名}-auto-translate-{时间戳}`)
+6. 提交翻译结果到新分支
+
+## 📝 注意事项
+
+- 翻译脚本需要 `OPENAI_API_KEY` 环境变量
+- 工作流会自动跳过已包含 `auto-translate` 的分支,避免循环触发
+- 翻译提示词已优化,确保专有名词和术语的一致性
+
+## ✅ 测试
+
+- [x] 中文 changelog 显示正常
+- [x] 韩文 changelog 显示正常,无解析错误
+- [x] 所有 UI 元素已正确本地化
+- [x] 日期格式统一
+- [x] 专有名词处理正确
+- [x] 自动翻译工作流正常运行
diff --git a/cn/changelog/2025.mdx b/cn/changelog/2025.mdx
index d052d64..a226888 100644
--- a/cn/changelog/2025.mdx
+++ b/cn/changelog/2025.mdx
@@ -1,8 +1,10 @@
---
title: "变更日志"
-description: "此日志记录了 2025 年 Codatta 所有更新、修复和新功能。"
+description: "本文档记录了 Codatta 在 2025 年的所有更新、修复和新功能。"
---
+import { useState, useEffect } from 'react';
+
## Dec 04, 2025
+
---
**Evolving Incentives: Reward Lock-ups for Long-term Ecosystem Management**
diff --git a/ko/changelog/2025.mdx b/ko/changelog/2025.mdx
new file mode 100644
index 0000000..154a7d1
--- /dev/null
+++ b/ko/changelog/2025.mdx
@@ -0,0 +1,847 @@
+---
+title: "변경 로그"
+description: "이 변경 로그는 2025년 Codatta의 모든 업데이트, 수정 및 새로운 기능을 문서화합니다."
+---
+
+import { useState, useEffect } from 'react';
+
+
+
+
+
+
+
+ {(() => {
+ const ShowResult = () => {
+ const [num, setNum] = useState(0);
+ useEffect(() => {
+ if (typeof document === 'undefined') return;
+ const update = () => {
+ try {
+ const items = document.querySelectorAll('.changelog-item');
+ let count = 0;
+ items.forEach(item => {
+ if (item.style.display !== 'none') count++;
+ });
+ setNum(count);
+ } catch {}
+ };
+ update();
+ const id = setInterval(update, 2000);
+ return () => clearInterval(id);
+ }, []);
+ return (
+
+
+ {num}
+ 개 결과
+
+ );
+ };
+ return
;
+ })()}
+
+
+
+## 2025년 12월 04일
+
+---
+**진화하는 인센티브: 장기 생태계 관리를 위한 보상 잠금 기능**
+
+
+
+
+
+고인센티브 작업(예: Airdrop 또는 Frontier 캠페인)에서의 보상을 위한 선택적 잠금 기능입니다.
+
+- **트리거:** 캠페인 후 보상이 귀하의 잔액에 입금됩니다.
+- **잠금:** 자산 페이지를 통해 스마트 계약에 보상을 잠금으로써 안전하게 보호합니다.
+- **해제:** 잠금 기간이 끝난 후, 한 번의 클릭으로 보상을 지갑으로 직접 청구할 수 있습니다.
+
+이는 우리의 인센티브 시스템을 세분화된 관리로 발전시키는 중요한 단계입니다. 이는 고보상 시나리오에서 유동성을 관리할 수 있는 도구를 제공하여 단기 참여와 생태계의 장기 건강을 균형 있게 유지합니다.
+
+
+
+
+
+## 2025년 11월 24일
+---
+**캠페인 시작: Airdrop 시즌 2**
+
+
+
+
+
+물리학, 금융 및 다중 모드 분야에서 학술 수준의 데이터셋을 체계적으로 수집하고, 세 개의 새로운 Frontier를 위한 고품질 데이터 파이프라인을 구축합니다.
+
+- **행동:** 100만 $XNY 상금 풀로 Airdrop 시즌 2를 시작했습니다(3개월 선형 분배).
+- **새로운 Frontier:**
+ - **고급 물리학 문제:** 현재 AI가 올바르게 답변할 수 없는 고난이도 물리학 문제를 제출하기 위한 도메인 전문가를 위한 것입니다.
+ - **암호화폐 및 주식 정보:** AI에 암호화폐 및 주식 투자 결정과 관련된 신뢰할 수 있는 정보를 제공합니다.
+ - **실제 사진:** 주석이 달린 메타데이터를 포함한 실제 사진을 AI에 제공합니다.
+- **보상 구조:** 작업 완료에 대해 90%, 리더보드 순위에 대해 10%를 지급합니다.
+- **일정:** 2025년 11월 24일 09:00 UTC – 2025년 12월 08일 09:00 UTC.
+
+
+
+
+
+## 2025년 11월 05일
+---
+**계정 시스템 업그레이드: DID 공식 출시**
+
+
+
+
+
+탈중앙화 식별자(DID)에 계정을 바인딩할 수 있는 기본 업그레이드입니다.
+
+- **바인딩:** Codatta에 다음 로그인 시 온체인 DID의 셀프 서비스 통합이 가능합니다.
+- **연관:** 바인딩된 DID는 모든 작업 행동, 데이터 기여 및 보상 분배를 체계적으로 인덱싱하는 통합 기본 키 역할을 합니다.
+- **계보:** 온체인 및 오프체인 활동의 완전한 추적 가능성과 검증을 가능하게 하여 불변의 데이터 출처를 확립합니다.
+
+이 배포는 검증 가능한 데이터 경제를 위한 기술적 초석을 마련합니다. 각 기여자에게 지속적이고 고유한 디지털 정체성을 제공함으로써 세분화된 기여 귀속, 명확한 소유권 검증 및 프로토콜 수준에서의 투명한 인센티브 할당을 가능하게 합니다.
+
+
+
+
+
+## 2025년 11월 03일
+---
+**퀘스트 시스템 조정: Crypto Frontier QUEST 모듈 서비스 종료**
+
+
+
+
+
+전체 Crypto Frontier QUEST 모듈과 모든 관련 작업(제출, 검증 및 보상 사냥 포함)이 오프라인 상태로 전환되었습니다.
+
+- **이유:** 이 모듈은 현재 제품 로드맵에 따라 계획된 생애 주기를 완료했습니다. 서비스 종료는 시스템 간소화 및 향후 기능 출시를 위한 자원 재조정을 지원합니다.
+- **참고:** 사용자의 과거 기여 기록과 이 모듈에서 얻은 보상은 그대로 유지됩니다. 이 변경으로 인해 다른 Frontier 모듈에는 영향을 미치지 않습니다.
+
+
+
+
+
+## 2025년 10월 24일
+---
+**Frontier 시스템 조정: Robotics Frontier 서비스 종료**
+
+
+
+
+
+Robotics Frontier가 공식적으로 서비스 종료되었습니다. 이 조치는 프론트엔드 진입점에만 영향을 미칩니다. 모든 과거 기여 데이터는 그대로 유지되며 접근 가능합니다.
+
+- **이유:** 이 결정은 자원과 사용자 주의를 가장 우선 순위가 높은 활성 Frontier에 집중하기 위한 지속적인 제품 간소화의 일환입니다. Robotics Frontier는 계획된 탐색 단계를 완료하였으며, 서비스 종료를 통해 노력을 통합할 수 있습니다.
+- **참고:** 사용자의 과거 기여 및 Robotics Frontier에서의 보상은 보존되며 기여 기록에서 검토할 수 있습니다. 다른 Frontier나 플랫폼 기능에는 영향을 미치지 않습니다.
+
+
+
+
+
+## 2025년 10월 23일
+---
+**스팸 방지 강화: 일일 제출 한도 구현**
+
+
+
+
+
+작업 스팸을 방지하기 위한 새로운 플랫폼 전반의 보안 정책입니다.
+
+- **방법:** 각 작업에 대해 구성 가능한 일일 제출 한도를 시행합니다.
+- **이유:** 시스템 자원을 보호하고 모든 기여자에게 공정한 참여를 보장하며 작업 생태계의 장기적인 건강과 공정성을 유지하기 위해서입니다.
+
+
+
+
+
+## 2025년 10월 20일
+---
+**Frontier 시스템 조정: Crypto Frontier 서비스 종료**
+
+
+
+
+
+Crypto Frontier가 공식적으로 서비스 종료되었습니다. 이 조치는 프론트엔드 진입점에만 영향을 미칩니다. 모든 과거 기여 데이터는 그대로 유지되며 접근 가능합니다.
+
+- **이유:** 이 결정은 자원과 사용자 주의를 가장 우선 순위가 높은 활성 Frontier에 집중하기 위한 지속적인 제품 간소화의 일환입니다. Crypto Frontier는 계획된 탐색 단계를 완료하였으며, 서비스 종료를 통해 노력을 통합할 수 있습니다.
+- **참고:** 사용자의 과거 기여 및 Crypto Frontier에서의 보상은 보존되며 기여 기록에서 검토할 수 있습니다. 다른 Frontier나 플랫폼 기능에는 영향을 미치지 않습니다.
+
+
+
+
+
+## 2025년 10월 15일
+---
+**시스템 거버넌스: 플랫폼 블랙리스트 제어 구현**
+
+
+
+
+
+플랫폼에 새로운 블랙리스트 제어 기능이 구현되었습니다.
+
+- **방법:** 지정된 관리자가 이제 악의적인 계정을 제한하기 위해 블랙리스트 규칙을 적용할 수 있습니다.
+- **이유:** 플랫폼 보안을 강화하고 기여자의 이익을 보호하며 생태계의 공정성을 유지하기 위해서입니다.
+
+
+
+
+
+## 2025년 10월 13일
+---
+**캠페인 시작: Airdrop 시즌 1**
+
+
+
+
+
+고보상 메커니즘과 스팸 방지 시행을 통해 고품질의 구조화된 데이터를 체계적으로 수집하고, 유효성과 학술 가치를 보장하며, 장기 기여자에게 보상하고 플랫폼의 데이터 생태계를 다각화하기 위해 다섯 개의 새로운 Frontier 모듈을 도입합니다.
+
+- **행동:** 250만 $XNY 상금 풀과 포인트 보상으로 Airdrop 시즌 1을 시작했습니다.
+- **새로운 Frontier:**
+ - **모델 비교:** 다양한 AI 모델 간의 성능 지표와 비즈니스 결과를 비교하여 최적의 솔루션이나 하이브리드 솔루션을 식별합니다.
+ - **LLM의 실수 발견:** LLM 출력에서의 추론, 사실 및 논리적 오류를 식별하여 모델 최적화를 지원합니다.
+ - **LLM의 실수 수정:** LLM이 실수에서 학습하고 자기 수정 및 추론을 개선할 수 있도록 교정 데이터를 수집합니다.
+ - **식품 과학:** 식품 과학 및 영양 기능에 대한 연구 데이터를 수집하여 이 분야의 혁신을 촉진합니다.
+ - **Lifelog Canvas:** 개인 행동 및 건강 관련 데이터를 추적하고 기록하여 행동 및 관련 연구를 지원합니다.
+- **보상 구조:** 보상은 제출 평가에 따라 순위가 매겨지고 캠페인 후 분배됩니다. 악의적인 제출은 데이터 품질을 보장하기 위해 처벌됩니다.
+- **일정:** 2025년 10월 13일 09:00 UTC - 2025년 10월 27일 09:00 UTC.
+
+
+
+
+
+## 2025년 10월 10일
+---
+**기능 출시: Frontier별 보상 활동**
+
+
+
+
+
+Frontier를 위한 구성 가능한 보상 활동이 이제 제공되어 추가적인 고부가가치 인센티브를 제공합니다.
+
+- **방법:** 선택된 Frontier 작업은 기본 포인트와는 독립적으로 $XNY 또는 USDT로 추가 보상을 제공합니다. 활동 매개변수(보상 금액, 기간, 목표)는 각 Frontier에 따라 설정되며 해당 홈페이지에 표시됩니다.
+- **이유:** 목표 지향적인 인센티브를 통해 고부가가치 데이터 작업에 대한 참여를 촉진하고, 데이터 품질과 생태계 참여를 개선하기 위함입니다.
+
+
+
+
+
+## 2025년 09월 26일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 3 4주차**
+
+
+
+
+
+신뢰할 수 있는 다중 도메인 데이터 수집을 통해 AI 개발을 가속화하고, 커뮤니티 주도의 분산형 AI 지식 구축을 촉진합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 09월 26일 07:00 UTC – 2025년 10월 03일 07:00 UTC.
+
+
+
+
+
+## 2025년 09월 12일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 3 3주차**
+
+
+
+
+
+AI 훈련 및 검증을 위해 생명, 로봇공학, 암호화폐, 모델 비교 및 지문 검증에 걸친 교차 도메인 데이터를 수집합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 09월 12일 07:00 UTC – 2025년 09월 19일 07:00 UTC.
+
+
+
+
+
+## 2025년 09월 05일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 3 2주차**
+
+
+
+
+
+AI 훈련 및 검증을 위해 생명, 로봇공학 및 암호화폐에 걸친 교차 도메인 데이터를 수집하고 주석을 달기 위해 진행됩니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 09월 05일 07:00 UTC – 2025년 09월 12일 07:00 UTC.
+
+
+
+
+
+## 2025년 08월 28일
+---
+**기능 출시: 사용자 데이터 프로필**
+
+
+
+
+
+사용자 정보 모듈 내에 새로운 데이터 프로필 기능을 도입합니다.
+
+- **방법:** 사용자 정보 > 데이터 프로필을 통해 총 제출 수, 획득한 보상 및 기여 통계를 시각적 대시보드에서 확인할 수 있습니다.
+- **이유:** 기여에 대한 투명성을 제공하고, 가시적인 진행 추적을 통해 참여를 강화하며, 장기적인 참여를 지원하기 위함입니다.
+
+
+
+
+
+## 2025년 08월 22일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 3 1주차**
+
+
+
+
+
+분산형 AI 훈련을 위해 생명, 로봇공학 및 암호화폐에 걸친 교차 도메인 주석 데이터의 기초를 마련합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 08월 22일 07:00 UTC – 2025년 08월 29일 07:00 UTC.
+
+
+
+
+
+## 2025년 08월 15일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 2 4주차**
+
+
+
+
+
+분산형 AI 훈련을 위해 음식 AI, 로봇 상호작용 및 CEX 온체인 데이터에 대한 도메인별 데이터 세트를 체계적으로 주석 달고 확장합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 08월 15일 07:00 UTC – 2025년 08월 22일 07:00 UTC.
+
+
+
+
+
+## 2025년 08월 08일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 2 3주차**
+
+
+
+
+
+프로젝트 학습, 음식 AI 판단, 로봇 주석 및 CEX 데이터 확장을 포함한 네 가지 주요 영역에서 체계적으로 주석이 달린 데이터를 수집합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 08월 08일 07:00 UTC – 2025년 08월 15일 07:00 UTC.
+
+
+
+
+
+## 2025년 08월 01일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 2 2주차**
+
+
+
+
+
+AI 음식 모델 비교, 로봇 상호작용 주석 및 CEX 핫 월렛 데이터 라벨링을 포함한 세 가지 핵심 도메인에서 주석이 달린 데이터를 수집합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 08월 01일 07:00 UTC – 2025년 08월 08일 07:00 UTC.
+
+
+
+
+
+## 2025년 07월 24일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 2 1주차**
+
+
+
+
+
+주석이 달린 실제 상호작용 데이터를 통해 AI 음식 분석을 검증하고 로봇 훈련을 향상시킵니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY(잠금 기간 포함), 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 07월 24일 07:00 UTC – 2025년 07월 31일 07:00 UTC.
+
+
+
+
+
+## 2025년 07월 16일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 1 4주차**
+
+
+
+
+
+무게, 조리 방법 및 칼로리 함량을 포함한 풍부한 주석을 수집하여 AI의 음식 이해를 심화합니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY, 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 07월 16일 13:00 UTC – 2025년 07월 23일 13:00 UTC.
+
+
+
+
+
+## 2025년 07월 09일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 1 3주차**
+
+
+
+
+
+문화적 및 맥락적 관련성을 넘어 단순한 라벨을 넘어서는 즉석 식품의 주석이 달린 이미지를 수집하여 AI에게 인간의 식습관에 대한 미세한 이해를 가르칩니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능(Alpha Points ≥ 61 필요).
+- **보상:** 총 보상 풀 50,000,000 $XNY, 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 07월 09일 13:00 UTC – 2025년 07월 16일 13:00 UTC.
+
+
+
+
+
+## 2025년 07월 07일
+---
+**기능 최적화: 퀘스트 시스템 복원**
+
+
+
+
+
+퀘스트 시스템이 완전히 복원되어 재개되었습니다.
+
+- **방법:** 2025년 06월 30일에 일시적으로 오프라인 상태였으며, 현재 포괄적으로 복원되었습니다.
+- **이유:** 아키텍처 업그레이드를 통해 시스템 안정성과 사용자 경험을 향상시키기 위함입니다.
+
+
+
+
+
+## 2025년 07월 02일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 1 2주차**
+
+
+
+
+
+채식, 비채식 및 혼합 카테고리 전반에 걸쳐 세밀한 이미지 라벨링을 통해 AI의 글로벌 음식 문화 및 선호도 이해를 확장하기 위함입니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능 (Alpha Points ≥ 61).
+- **보상:** 총 50,000,000 $XNY의 보상 풀, 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 07월 02일 13:00 UTC – 2025년 07월 09일 13:00 UTC.
+
+
+
+
+
+## 2025년 06월 25일
+---
+**캠페인 시작: Codatta Booster 캠페인 시즌 1 1주차**
+
+
+
+
+
+주석이 달린 음식 데이터를 수집하고, 퀴즈를 통해 커뮤니티 지식을 활용하며, 다단계 인센티브 구조를 통해 참여를 보상함으로써 첫 번째 시즌을 시작하기 위함입니다.
+
+- **접근:** Binance Wallet Booster 탭 또는 홈페이지 배너를 통해 접근 가능 (Alpha Points ≥ 61).
+- **보상:** 총 50,000,000 $XNY의 보상 풀, 작업 완료 및 검증 후 분배됩니다.
+- **일정:** 2025년 06월 25일 13:00 UTC – 2025년 07월 02일 13:00 UTC.
+
+
{/* Component definitions - moved to end of file for cleaner code organization */}
+export const ChangelogFilter = () => {
+ const [activeFilter, setActiveFilter] = useState('all');
+ const [isOpen, setIsOpen] = useState(false);
+ const [isMenuHovered, setIsMenuHovered] = useState(false);
+
+ useEffect(() => {
+ window.activeTypeFilter = activeFilter;
+ }, [activeFilter]);
+
+ useEffect(() => {
+ const items = document.querySelectorAll('.changelog-item');
+ const activeMonth = window.activeMonthFilter || 'all';
+
+ items.forEach((item) => {
+ const itemType = item.getAttribute('data-type');
+ const itemMonth = item.getAttribute('data-month');
+
+ const typeMatch = activeFilter === 'all' || itemType === activeFilter;
+ const monthMatch = activeMonth === 'all' || itemMonth === activeMonth;
+
+ item.style.display = typeMatch && monthMatch ? '' : 'none';
+ });
+ }, [activeFilter]);
+
+ const filterTypes = [
+ { id: 'all', label: '전체', color: '#6b7280', count: 24 },
+ { id: 'core-feature', label: '핵심 기능 출시', color: '#16A34A', count: 4 },
+ { id: 'optimization', label: '조정 및 최적화', color: '#F59E0B', count: 3 },
+ { id: 'fixes', label: '수정 및 기능 종료', color: '#EF4444', count: 3 },
+ { id: 'campaign', label: '캠페인 시작', color: '#A855F7', count: 14 }
+ ];
+
+ const activeType = filterTypes.find(type => type.id === activeFilter) || filterTypes[0];
+
+ return (
+
+
setIsOpen(!isOpen)}
+ style={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: '0.5rem',
+ padding: '0.625rem 1rem',
+ border: '2px solid #e5e7eb',
+ borderRadius: '0.5rem',
+ background: 'white',
+ cursor: 'pointer',
+ fontSize: '0.875rem',
+ fontWeight: '500',
+ boxShadow: isOpen ? `0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)` : '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+ transition: 'all 0.2s ease',
+ userSelect: 'none',
+ minWidth: '200px'
+ }}
+ >
+
+
+ {activeType.label} ({activeType.count})
+
+
+
+
+ {isOpen && (
+
setIsMenuHovered(true)}
+ onMouseLeave={() => setIsMenuHovered(false)}
+ style={{
+ position: 'absolute',
+ top: '100%',
+ left: 0,
+ marginTop: '0.5rem',
+ backgroundColor: 'white',
+ border: `2px solid ${isMenuHovered ? '#9ca3af' : '#e5e7eb'}`,
+ borderRadius: '0.5rem',
+ boxShadow: isMenuHovered
+ ? '0 10px 15px -3px rgba(0, 0, 0, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.1)'
+ : '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
+ zIndex: 1000,
+ minWidth: '280px',
+ overflow: 'hidden',
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease'
+ }}
+ >
+ {filterTypes.map((type) => (
+
{
+ setActiveFilter(type.id);
+ setIsOpen(false);
+ }}
+ style={{
+ padding: '0.75rem 1rem',
+ cursor: 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottom: type.id !== filterTypes[filterTypes.length - 1].id ? '1px solid #f3f4f6' : 'none',
+ backgroundColor: activeFilter === type.id ? '#f9fafb' : 'white',
+ transition: 'background-color 0.15s ease'
+ }}
+ onMouseEnter={(e) => {
+ if (activeFilter !== type.id) {
+ e.currentTarget.style.backgroundColor = '#f3f4f6';
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (activeFilter !== type.id) {
+ e.currentTarget.style.backgroundColor = 'white';
+ }
+ }}
+ >
+
+ {type.label}
+
+
+ {type.count}
+
+
+ ))}
+
+ )}
+
+ {isOpen && (
+
setIsOpen(false)}
+ style={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ zIndex: 999,
+ backgroundColor: 'transparent'
+ }}
+ />
+ )}
+
+ );
+};
+
+export const MonthFilter = () => {
+ const [activeMonth, setActiveMonth] = useState('all');
+ const [isOpen, setIsOpen] = useState(false);
+ const [isMenuHovered, setIsMenuHovered] = useState(false);
+
+ useEffect(() => {
+ window.activeMonthFilter = activeMonth;
+ const event = new Event('monthFilterChange');
+ window.dispatchEvent(event);
+ }, [activeMonth]);
+
+ useEffect(() => {
+ const handleMonthChange = () => {
+ const items = document.querySelectorAll('.changelog-item');
+ const activeType = window.activeTypeFilter || 'all';
+
+ items.forEach((item) => {
+ const itemType = item.getAttribute('data-type');
+ const itemMonth = item.getAttribute('data-month');
+
+ const typeMatch = activeType === 'all' || itemType === activeType;
+ const monthMatch = activeMonth === 'all' || itemMonth === activeMonth;
+
+ item.style.display = typeMatch && monthMatch ? '' : 'none';
+ });
+ };
+
+ window.addEventListener('monthFilterChange', handleMonthChange);
+ handleMonthChange();
+
+ return () => {
+ window.removeEventListener('monthFilterChange', handleMonthChange);
+ };
+ }, [activeMonth]);
+
+ const months = [
+ { id: 'all', label: '전체', count: 24 },
+ { id: 'dec', label: '12월', count: 1 },
+ { id: 'nov', label: '11월', count: 3 },
+ { id: 'oct', label: '10월', count: 6 },
+ { id: 'sep', label: '9월', count: 3 },
+ { id: 'aug', label: '8월', count: 5 },
+ { id: 'jul', label: '7월', count: 5 },
+ { id: 'jun', label: '6월', count: 1 }
+ ];
+
+ const activeMonthData = months.find(month => month.id === activeMonth) || months[0];
+
+ return (
+
+
setIsOpen(!isOpen)}
+ style={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: '0.5rem',
+ padding: '0.625rem 1rem',
+ border: '2px solid #e5e7eb',
+ borderRadius: '0.5rem',
+ background: 'white',
+ cursor: 'pointer',
+ fontSize: '0.875rem',
+ fontWeight: '500',
+ boxShadow: isOpen ? `0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)` : '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+ transition: 'all 0.2s ease',
+ userSelect: 'none',
+ minWidth: '200px'
+ }}
+ >
+
+ {activeMonthData.label} ({activeMonthData.count})
+
+
+
+
+ {isOpen && (
+
setIsMenuHovered(true)}
+ onMouseLeave={() => setIsMenuHovered(false)}
+ style={{
+ position: 'absolute',
+ top: '100%',
+ left: 0,
+ marginTop: '0.5rem',
+ backgroundColor: 'white',
+ border: `2px solid ${isMenuHovered ? '#9ca3af' : '#e5e7eb'}`,
+ borderRadius: '0.5rem',
+ boxShadow: isMenuHovered
+ ? '0 10px 15px -3px rgba(0, 0, 0, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.1)'
+ : '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
+ zIndex: 1000,
+ minWidth: '280px',
+ overflow: 'hidden',
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease'
+ }}
+ >
+ {months.map((month) => (
+
{
+ setActiveMonth(month.id);
+ setIsOpen(false);
+ }}
+ style={{
+ padding: '0.75rem 1rem',
+ cursor: 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottom: month.id !== months[months.length - 1].id ? '1px solid #f3f4f6' : 'none',
+ backgroundColor: activeMonth === month.id ? '#f9fafb' : 'white',
+ transition: 'background-color 0.15s ease'
+ }}
+ onMouseEnter={(e) => {
+ if (activeMonth !== month.id) {
+ e.currentTarget.style.backgroundColor = '#f3f4f6';
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (activeMonth !== month.id) {
+ e.currentTarget.style.backgroundColor = 'white';
+ }
+ }}
+ >
+
+ {month.label}
+
+
+ {month.count}
+
+
+ ))}
+
+ )}
+
+ {isOpen && (
+
setIsOpen(false)}
+ style={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ zIndex: 999,
+ backgroundColor: 'transparent'
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/translate.js b/translate.js
new file mode 100644
index 0000000..091c9b2
--- /dev/null
+++ b/translate.js
@@ -0,0 +1,239 @@
+import OpenAI from "openai";
+import fs from "fs-extra";
+import path from "path";
+
+/**
+ * Configuration
+ */
+const SRC_DIR = "en/changelog";
+const TARGET_LANGS = [
+ {
+ code: "cn",
+ name: "Chinese",
+ systemPrompt:
+ "请将以下英文 changelog 按中文语境重写一下,要求:1. 只翻译纯文本部分,忽略任何 HTML 标签、代码块、表格、特殊格式(如代码行、列)等,看着像代码也保留不动。2. 保留原有 HTML 标签和结构,不要修改格式。3. 保证翻译内容准确。4.小标题的单词也要翻译,日期也要翻译,但必须遵循统一的日期格式。5.不要直译,要理解英文原文的语义,然后用符合中文语言习惯的自然方式重新表述。例如:'action' 不应直译为'行动',而应根据上下文用更自然的中文表达(如'操作'、'动作'等)。6.专有名词识别规则:- 自动识别首字母大写的专有名词(如产品名、模块名、功能名等),这些通常应保持英文不翻译;- 特别地,以下术语和表达必须固定使用,不要翻译:- 'Frontier' 和 'Frontiers' 是产品名,保持英文不翻译;- 'Crypto Frontier'、'Crypto Frontier QUEST'、'Robotics Frontier' 是专有名词,保持英文不翻译;- 'Model Comparison'、'Spot LLM's Mistakes'、'Correct LLM's Mistakes'、'Food Science'、'Lifelog Canvas' 是专有名词,保持英文不翻译;- 'Lineage' 翻译为'血缘'(因为我们有产品叫 Data Lineage 数据血缘);- 小要点中的 'How' 不要翻译成'如何',统一翻译为'运作方式';- 'Timeline' 不要翻译成'时间表'、'时间安排',统一翻译为'活动时间';- 'Access' 不要翻译成'访问'、'访问方式',统一翻译为'参与方式';- 'Lock' 统一翻译为'锁仓'。7.日期格式必须严格统一为:'2025 年 12 月 04 日'格式(汉字和数字之间必须保留 1 个空格,年份、月份、日期都是两位数,月份和日期不足两位要补零,例如:'2025 年 09 月 05 日'、'2025 年 06 月 25 日')。所有日期标题(如 '## Dec 04, 2025')必须翻译为 '## 2025 年 12 月 04 日' 格式。确保翻译后的中文读起来自然流畅,符合中文表达习惯。",
+ },
+ {
+ code: "ko",
+ name: "Korean",
+ systemPrompt:
+ "다음 영어 changelog 를 한국어 문맥에 맞게 재작성해 주세요. 다음 요구사항을 엄격히 준수하세요: 1. 텍스트 내용만 번역하고, HTML 태그, 코드 블록, 표, 특수 형식(예: 코드 행, 열 등) 등은 무시하고, 코드로 보이는 모든 내용은 그대로 유지하세요. 2. 원본 HTML 태그와 구조를 유지하고, 형식을 수정하지 마세요. 3. 번역 내용의 정확성을 보장하세요. 4. 소제목의 단어도 반드시 번역하세요. 날짜도 번역해야 하며, 반드시 통일된 날짜 형식을 따라야 합니다. 5. 직역하지 말고, 영어 원문의 의미를 이해한 후 한국어 언어 습관에 맞는 자연스러운 방식으로 재표현하세요. 예를 들어, 'action'을 단순히 '행동'으로 직역하지 말고, 문맥에 따라 더 자연스러운 한국어 표현을 사용하세요. 6. 고유명사 식별 규칙: - 대문자로 시작하는 고유명사(예: 제품명, 모듈명, 기능명 등)를 자동으로 식별하고, 이러한 용어는 일반적으로 영어로 유지하고 번역하지 마세요. - 특히 다음 용어와 표현은 고정적으로 사용해야 하며 번역하지 마세요: - 'Frontier'와 'Frontiers'는 제품명이므로 영어로 유지하세요. - 'Crypto Frontier', 'Crypto Frontier QUEST', 'Robotics Frontier'는 고유명사이므로 영어로 유지하세요. - 'Model Comparison', 'Spot LLM's Mistakes', 'Correct LLM's Mistakes', 'Food Science', 'Lifelog Canvas'는 고유명사이므로 영어로 유지하세요. 7. 날짜 형식은 반드시 '2025년 12월 04일' 형식으로 통일하세요(년, 월, 일은 모두 두 자리 숫자이며, 월과 일이 한 자리인 경우 앞에 0을 붙여야 합니다. 예: '2025년 09월 05일', '2025년 06월 25일'). 모든 날짜 제목(예: '## Dec 04, 2025')은 '## 2025년 12월 04일' 형식으로 번역해야 합니다. 번역된 한국어가 자연스럽고 유창하게 읽히도록 한국어 표현 습관에 맞게 작성하세요.",
+ },
+];
+
+// Initialize OpenAI client
+const client = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+ timeout: 120000,
+ maxRetries: 0,
+});
+
+/**
+ * Retry strategy
+ */
+async function withRetry(fn, maxRetries = 5) {
+ let retries = 0;
+ while (retries < maxRetries) {
+ try {
+ return await fn();
+ } catch (err) {
+ retries++;
+ if (retries >= maxRetries) {
+ throw new Error(`Failed after ${maxRetries} retries: ${err.message}`);
+ }
+ const delay = 1000 * Math.pow(2, retries);
+ console.log(`Request failed, retrying after ${delay}ms (attempt ${retries}/${maxRetries}):`, err.message);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+}
+
+/**
+ * Split text into chunks (only for the part to be translated)
+ */
+function splitTextByParagraphs(text, maxChars = 8000) {
+ const paragraphs = text.split("\n\n");
+ const chunks = [];
+ let currentChunk = "";
+
+ for (const para of paragraphs) {
+ if (para.length > maxChars) {
+ const subPara = para.split("\n");
+ let subCurrent = "";
+ for (const sub of subPara) {
+ if (subCurrent.length + sub.length <= maxChars) {
+ subCurrent += sub + "\n";
+ } else {
+ chunks.push(subCurrent.trim());
+ subCurrent = sub + "\n";
+ }
+ }
+ if (subCurrent.trim()) chunks.push(subCurrent.trim());
+ continue;
+ }
+
+ if (currentChunk.length + para.length <= maxChars) {
+ currentChunk += para + "\n\n";
+ } else {
+ chunks.push(currentChunk.trim());
+ currentChunk = para + "\n\n";
+ }
+ }
+ if (currentChunk.trim()) {
+ chunks.push(currentChunk.trim());
+ }
+ console.log(`✅ Split translation part into ${chunks.length} chunks, max ${maxChars} chars per chunk`);
+ return chunks;
+}
+
+/**
+ * Two-marker truncation logic (core modification)
+ * Rules:
+ * 1. Before markerBefore (inclusive) → keep as-is, no translation
+ * 2. Between markerBefore and markerAfter → translate
+ * 3. After markerAfter (inclusive) → keep as-is, no translation
+ */
+function truncateWithTwoMarkers(text, markerBefore, markerAfter) {
+ // 1. Locate markerBefore (supports multi-line)
+ const markerBeforeIndex = text.indexOf(markerBefore);
+ // 2. Locate markerAfter (search forward, must be after markerBefore)
+ const markerAfterIndex = markerBeforeIndex === -1
+ ? -1
+ : text.indexOf(markerAfter, markerBeforeIndex + markerBefore.length);
+
+ // Edge case 1: markerBefore not found → only handle markerAfter (keep after markerAfter as-is)
+ if (markerBeforeIndex === -1) {
+ if (markerAfterIndex === -1) {
+ console.log("⚠️ No markers found, will translate entire content");
+ return { translatePart: text, keepBefore: "", keepAfter: "" };
+ }
+ console.log("⚠️ markerBefore not found, keeping content after markerAfter as-is");
+ return {
+ translatePart: text.slice(0, markerAfterIndex).trim(),
+ keepBefore: "",
+ keepAfter: text.slice(markerAfterIndex)
+ };
+ }
+
+ // Edge case 2: markerBefore found but markerAfter not found → keep before markerBefore as-is, translate the rest
+ if (markerAfterIndex === -1) {
+ console.log("⚠️ markerAfter not found, keeping content before markerBefore as-is");
+ return {
+ translatePart: text.slice(markerBeforeIndex + markerBefore.length).trim(),
+ keepBefore: text.slice(0, markerBeforeIndex + markerBefore.length),
+ keepAfter: ""
+ };
+ }
+
+ // Normal case: both markers found → translate the middle part
+ console.log(`✅ Both markers located:
+ - markerBefore position: ${markerBeforeIndex}
+ - markerAfter position: ${markerAfterIndex}`);
+
+ return {
+ // To translate: between markerBefore and markerAfter
+ translatePart: text.slice(markerBeforeIndex + markerBefore.length, markerAfterIndex).trim(),
+ // Keep: before markerBefore (inclusive)
+ keepBefore: text.slice(0, markerBeforeIndex + markerBefore.length),
+ // Keep: after markerAfter (inclusive)
+ keepAfter: text.slice(markerAfterIndex)
+ };
+}
+
+/**
+ * Translation function (integrates two-marker + chunking + translation + concatenation)
+ */
+async function translate(text, systemPrompt) {
+ console.log("\n📝 Original text total length:", text.length, "characters");
+
+ // Configure two markers (exact copy, including newlines/indentation/special characters)
+ // markerBefore: }; return
; })()}
+ const markerBefore = `};
+ return
;
+ })()}
+
`;
+ // markerAfter: {/* Component definitions - moved to end of file for cleaner code organization */}
+ const markerAfter = `{/* Component definitions - moved to end of file for cleaner code organization */}`;
+
+ // Execute two-marker truncation
+ const { translatePart, keepBefore, keepAfter } = truncateWithTwoMarkers(text, markerBefore, markerAfter);
+
+ // No content to translate → return kept parts directly
+ if (!translatePart) {
+ return keepBefore + keepAfter;
+ }
+
+ // Chunk and translate the middle content
+ const chunks = splitTextByParagraphs(translatePart);
+ const translatedChunks = [];
+
+ for (let i = 0; i < chunks.length; i++) {
+ console.log(`🔄 Translating chunk ${i+1}/${chunks.length} (${chunks[i].length} characters)`);
+ const res = await withRetry(async () => {
+ return await client.chat.completions.create({
+ model: "gpt-4o-mini",
+ messages: [
+ { role: "system", content: systemPrompt },
+ { role: "user", content: `Please translate the following text, strictly following the system instructions:\n${chunks[i]}` },
+ ],
+ temperature: 0.0,
+ max_tokens: 4096,
+ stream: false,
+ });
+ });
+
+ if (!res || !res.choices || res.choices.length === 0) {
+ throw new Error(`Translation failed for chunk ${i+1}: API returned abnormal response`);
+ }
+ translatedChunks.push(res.choices[0].message.content.trim());
+ }
+
+ // Concatenate final result: keepBefore + translated middle content + keepAfter
+ const translatedPart = translatedChunks.join("\n\n");
+ const finalResult = keepBefore + (translatedPart ? "\n" + translatedPart : "") + keepAfter;
+
+ return finalResult;
+}
+
+/**
+ * Main process
+ */
+async function run() {
+ if (!(await fs.pathExists(SRC_DIR))) {
+ console.log("❌ changelog directory not found, skipping translation");
+ return;
+ }
+
+ const files = await fs.readdir(SRC_DIR);
+ for (const file of files) {
+ if (!file.endsWith(".md") && !file.endsWith(".mdx")) continue;
+
+ const srcPath = path.join(SRC_DIR, file);
+ const content = await fs.readFile(srcPath, "utf-8");
+
+ console.log(`\n========== Processing ${srcPath} ==========`);
+
+ for (const lang of TARGET_LANGS) {
+ const outDir = path.join(lang.code, "changelog");
+ const outPath = path.join(outDir, file);
+ await fs.ensureDir(outDir);
+
+ try {
+ const translated = await translate(content, lang.systemPrompt);
+ await fs.writeFile(outPath, translated, "utf-8");
+ console.log(`✅ Success: ${file} → ${lang.code}/changelog/${file}`);
+ } catch (err) {
+ console.error(`❌ Failed: ${file} → ${lang.code}`, err.stack);
+ continue;
+ }
+ }
+ }
+
+ console.log("\n🎉 All files processed!");
+}
+
+// Execute main process
+run().catch((err) => {
+ console.error("💥 Global execution failed:", err.stack);
+ process.exit(1);
+});
\ No newline at end of file