references/config/extend-schema.md
# EXTEND.md Schema for baoyu-translate
YAML format:
```yaml
# Default target language (ISO code or common name)
target_language: zh-CN
# Default translation mode
default_mode: normal # quick | normal | refined
# Target audience (affects annotation depth and register)
audience: general # general | technical | academic | business | or custom string
# Translation style preference
style: storytelling # storytelling | formal | technical | literal | academic | business | humorous | conversational | elegant | or custom string
# Word count threshold to trigger chunked translation
chunk_threshold: 4000
# Max words per chunk
chunk_max_words: 5000
# Custom glossary (merged with built-in glossary)
# CLI --glossary flag overrides these
# Supports inline entries and/or file paths
glossary:
- from: "Reinforcement Learning"
to: "强化学习"
- from: "Transformer"
to: "Transformer"
note: "Keep English"
# Load glossary from external file(s)
# Supports absolute path or relative to EXTEND.md location
# File format: markdown table with | from | to | note | columns,
# or YAML list of {from, to, note} entries
glossary_files:
- ./my-glossary.md
- /path/to/shared-glossary.yaml
# Language-pair specific glossaries
glossaries:
en-zh:
- from: "AI Agent"
to: "AI 智能体"
ja-zh:
- from: "人工知能"
to: "人工智能"
```
## Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `target_language` | string | `zh-CN` | Default target language code |
| `default_mode` | string | `normal` | Default translation mode (`quick` / `normal` / `refined`) |
| `audience` | string | `general` | Target reader profile (`general` / `technical` / `academic` / `business` / custom) |
| `style` | string | `storytelling` | Translation style (`storytelling` / `formal` / `technical` / `literal` / `academic` / `business` / `humorous` / `conversational` / `elegant` / custom) |
| `chunk_threshold` | number | `4000` | Word count threshold to trigger chunked translation |
| `chunk_max_words` | number | `5000` | Max words per chunk |
| `glossary` | array | `[]` | Universal glossary entries (inline) |
| `glossary_files` | array | `[]` | External glossary file paths (absolute or relative to EXTEND.md) |
| `glossaries` | object | `{}` | Language-pair specific glossary entries |
## Glossary Entry
| Field | Required | Description |
|-------|----------|-------------|
| `from` | yes | Source term |
| `to` | yes | Target translation |
| `note` | no | Usage note (e.g., "Keep English", "Only in tech context") |
## Glossary File Format
`glossary_files` accepts two formats (absolute paths or relative to EXTEND.md):
**Markdown table** (`.md`):
```markdown
| from | to | note |
|------|----|------|
| Reinforcement Learning | 强化学习 | |
| Transformer | Transformer | Keep English |
```
**YAML list** (`.yaml` / `.yml`):
```yaml
- from: "Reinforcement Learning"
to: "强化学习"
- from: "Transformer"
to: "Transformer"
note: "Keep English"
```
## Priority
1. CLI `--glossary` file entries
2. EXTEND.md `glossaries[pair]` entries
3. EXTEND.md `glossary` entries (inline)
4. EXTEND.md `glossary_files` entries (in listed order, later files override earlier)
5. Built-in glossary (e.g., `references/glossary-en-zh.md`)
Later entries override earlier ones for the same source term.
references/config/first-time-setup.md
---
name: first-time-setup
description: First-time setup flow for baoyu-translate preferences
---
# First-Time Setup
**BLOCKING**: runs when no EXTEND.md is found and must finish before any translation. Ask only these questions (don't start translating or ask about files/output paths), save EXTEND.md, then continue.
## Questions
Use the user's input language (or saved language preference). Ask all questions in ONE call:
### Question 1: Target Language
```yaml
header: "Target Language"
question: "Default target language?"
options:
- label: "简体中文 zh-CN (Recommended)"
description: "Translate to Simplified Chinese"
- label: "繁體中文 zh-TW"
description: "Translate to Traditional Chinese"
- label: "English en"
description: "Translate to English"
- label: "日本語 ja"
description: "Translate to Japanese"
```
Note: User may type a custom language code.
### Question 2: Translation Mode
```yaml
header: "Mode"
question: "Default translation mode?"
options:
- label: "Normal (Recommended)"
description: "Analyze content first, then translate"
- label: "Quick"
description: "Direct translation, no analysis"
- label: "Refined"
description: "Full workflow: analyze → translate → review → polish"
```
### Question 3: Target Audience
```yaml
header: "Audience"
question: "Default target audience?"
options:
- label: "General readers (Recommended)"
description: "Plain language, more translator's notes for jargon"
- label: "Technical"
description: "Developers/engineers, less annotation on tech terms"
- label: "Academic"
description: "Formal register, precise terminology"
- label: "Business"
description: "Business-friendly tone, explain tech concepts"
```
Note: User may type a custom audience description.
### Question 4: Translation Style
```yaml
header: "Style"
question: "Translation style?"
options:
- label: "Storytelling (Recommended)"
description: "Engaging narrative flow, smooth transitions"
- label: "Formal"
description: "Professional, structured, neutral tone"
- label: "Technical"
description: "Precise, documentation-style, concise"
- label: "Literal"
description: "Close to original structure"
- label: "Academic"
description: "Scholarly, rigorous, formal register"
- label: "Business"
description: "Concise, results-focused, action-oriented"
- label: "Humorous"
description: "Preserves humor, witty, playful"
- label: "Conversational"
description: "Casual, friendly, spoken-like"
- label: "Elegant"
description: "Literary, polished, aesthetically refined"
```
Note: User may type a custom style description.
### Question 5: Save Location
```yaml
header: "Save"
question: "Where to save preferences?"
options:
- label: "User (Recommended)"
description: "$HOME/.baoyu-skills/ (all projects)"
- label: "Project"
description: ".baoyu-skills/ (this project only)"
```
## Save Locations
| Choice | Path | Scope |
|--------|------|-------|
| User | `$HOME/.baoyu-skills/baoyu-translate/EXTEND.md` | All projects |
| Project | `.baoyu-skills/baoyu-translate/EXTEND.md` | Current project |
## After Setup
1. Create the directory if needed; write EXTEND.md with the selected values
2. Confirm: "Preferences saved to [path]"
3. Mention: "You can add custom glossary terms to EXTEND.md anytime. See the `glossary` section in the file for the format."
4. Continue the translation
## EXTEND.md Template
```yaml
target_language: [zh-CN/zh-TW/en/ja/...]
default_mode: [quick/normal/refined]
audience: [general/technical/academic/business/custom]
style: [storytelling/formal/technical/literal/academic/business/humorous/conversational/elegant]
# Custom glossary (optional) — add your own term translations here
# glossary:
# - from: "Term"
# to: "翻译"
# - from: "Another Term"
# to: "另一个翻译"
# note: "Usage context"
```
Users can edit EXTEND.md later, or delete it to rerun setup.
references/glossary-en-zh.md
# English → Chinese Glossary
Only non-obvious or easily mistranslated terms; straightforward ones (e.g., Machine Learning → 机器学习) are omitted.
| English | Chinese | Notes |
|---------|---------|-------|
| AI Agent | AI 智能体 | |
| Vibe Coding | 凭感觉编程 | |
| the Bitter Lesson | 苦涩的教训 | Rich Sutton's essay |
| Context Engineering | 上下文工程 | |
| AI Wrapper | AI 套壳 | |
| RLHF | 基于人类反馈的强化学习 | |
| Hallucination | 幻觉 | AI-specific meaning |
| Alignment | 对齐 | AI safety context |
| Guardrails | 护栏 | AI safety context |
| Agentic | 智能体化的 | |
| Grounding | 基础化/落地 | Context-dependent |
| Embedding | 嵌入/向量化 | Context-dependent |
| Moat | 护城河 | Business context |
| Flywheel | 飞轮效应 | |
| Boilerplate | 样板代码 | |
references/refined-workflow.md
# Translation Workflow Details
Per-step guidelines. All intermediate results are saved in the output directory.
## Step 1: Content Analysis
Save to `01-analysis.md`.
### 1.1 Content Summary
- What is this content about? What is the core argument?
- Author background, stance, and writing context
- Purpose and intended audience of the original
### 1.2 Terminology
- List technical terms, proper nouns, brand names, acronyms
- Cross-reference with loaded glossaries
- For terms not in glossary, determine standard translations
- Record in a terminology table
### 1.3 Tone & Style
- Formal or conversational? Humor, metaphor, cultural references?
- What register is appropriate for the translation given the target audience?
### 1.4 Translation Challenges
- **Comprehension gaps**: Terms or references that target readers may not understand — note what explanation is needed
- **Figurative language**: Metaphors, idioms, expressions that don't translate literally — note intended meaning and target-language approach (interpret / substitute / retain)
- **Structural challenges**: Long complex sentences, wordplay, puns, or humor that needs creative adaptation
**Save `01-analysis.md`** with:
```
## Content Summary
[Core argument, author, context, purpose]
## Terminology
[term → translation, ...]
## Tone & Style
[assessment]
## Translation Challenges
- [term/passage] → [challenge type] → [suggested approach]
- ...
```
## Step 2: Assemble Translation Prompt
Main agent builds the prompt from `01-analysis.md` using [references/subagent-prompt-template.md](subagent-prompt-template.md), inlining:
- **Target style**: Resolved style preset + source voice assessment from §1.3
- **Content background**: Summary from §1.1
- **Glossary**: Merged glossary with analysis-extracted terms from §1.2
- **Translation challenges**: All challenges from §1.4
Save to `02-prompt.md`; used by subagents (chunked) or the main agent (non-chunked).
## Step 3: Initial Draft
Save to `03-draft.md`. Chunked: merged from subagent chunk translations; non-chunked: the main agent translates directly. Follow `02-prompt.md` and the **Translation principles** in SKILL.md.
## Step 4: Critical Review
Main agent reviews the draft against the source → `04-critique.md`. **Diagnosis only** — no rewriting yet.
### 4.1 Accuracy
- Compare each paragraph against the original
- Verify facts, numbers, dates, proper nouns
- Flag content accidentally added, removed, or altered
- Check terminology consistency with glossary
### 4.2 Native Voice
- Flag sentences that read as "translated" rather than "written" — unnatural word order, calques, stiff phrasing
- For CJK targets: check for unnecessary connectives (因此/然而/此外), passive voice abuse (被/由/受到), noun pile-ups, over-nominalization
- Flag metaphors translated literally that sound unnatural in the target language
- Check emotional connotations are preserved, not flattened
- Note where sentence restructuring would improve readability
### 4.3 Notes & Adaptation
- Are translator's notes accurate, concise, and genuinely helpful?
- Flag missed comprehension challenges that need notes, and over-annotations on obvious terms
- Were translation strategies from `02-prompt.md` followed?
- Do cultural references work in the target language?
**Save `04-critique.md`** with:
```
## Accuracy
- [issue]: [location] — [description]
## Native Voice
- [issue]: [example] → [suggested fix]
## Notes & Adaptation
- [add/remove/revise]: [term/passage] — [reason]
## Summary
[Overall assessment: X critical issues, Y improvements]
```
## Step 5: Revision
Read `03-draft.md` and `04-critique.md`; apply all findings (fix accuracy issues, rewrite unnatural expressions, adjust notes, improve flow) → `05-revision.md`.
## Step 6: Polish
Final publication-quality pass on `05-revision.md` → `translation.md`:
- Read it as a standalone piece — does it flow as native content?
- Smooth remaining rough transitions
- Ensure consistent narrative voice and style throughout
- Final terminology consistency check
- Verify formatting is preserved correctly
## Chunked Refined Translation
Subagents (one per chunk, in parallel) only produce the Step 3 draft; the main agent does everything else:
1. Analysis (Step 1) on the **entire** document first → `01-analysis.md`
2. Assemble prompt → `02-prompt.md`
3. Split into chunks → `chunks/`
4. Subagents translate chunks (each reads `02-prompt.md`) → merge into `03-draft.md`
5. Review → `04-critique.md`; revise → `05-revision.md`; polish → `translation.md`
6. Final cross-chunk consistency check: terminology, narrative flow, transitions at chunk boundaries
references/subagent-prompt-template.md
# Subagent Translation Prompt Template
1. **`02-prompt.md`** — shared context saved to the output directory (background, glossary, challenges, principles; no task instructions). The main agent inlines relevant context from `01-analysis.md` (if it exists).
2. **Subagent spawn prompt** — task instructions per subagent: one per chunk, spawned in parallel (or one per source file if non-chunked).
Replace `{placeholders}`. Omit sections marked "if analysis exists" for quick mode.
---
## Part 1: `02-prompt.md` (shared context, saved as file)
```markdown
You are a professional translator. Your task is to translate markdown content from {source_lang} to {target_lang}.
## Target Audience & Style
**Audience**: {audience description}
**Target style**: {style description — e.g., "storytelling: engaging narrative flow, smooth transitions, vivid phrasing" or custom style from user}
**Source voice** (from analysis, if exists): {Brief description of the original author's voice — formal/conversational, humor, register, sentence rhythm.}
## Content Background
{Inlined from 01-analysis.md if analysis exists: content summary, core argument, author background, context.}
## Glossary
Apply these term translations consistently. First occurrence: include original in parentheses.
{Merged glossary — one per line: English → Translation}
## Translation Challenges
{Inlined from 01-analysis.md §1.4 if analysis exists. Comprehension gaps, figurative language, structural challenges with suggested approaches:}
- **{term/passage}**: {challenge type} → {suggested approach}
## Translation Principles
Rewrite the content into natural, engaging {target_lang} — not merely translate it. Every sentence should read as if a skilled native writer composed it from scratch.
- **Accuracy first**: Facts, data, and logic must match the original exactly
- **Natural flow**: Use idiomatic {target_lang} word order. Break long source sentences into shorter, natural ones. Interpret metaphors and idioms by intended meaning, not word-for-word
- **Terminology**: Use glossary translations consistently. Annotate with original in parentheses on first occurrence of specialized terms
- **Preserve format**: Keep all markdown formatting (headings, bold, italic, images, links, code blocks)
- **Proactive interpretation**: For jargon or concepts the target audience may lack context for, add concise explanations in **bold parentheses** `(**解释**)`. Keep annotations few — only where genuinely needed
```
---
## Part 2: Subagent spawn prompt (passed as Agent tool prompt)
### Chunked mode (one subagent per chunk, all spawned in parallel)
```
Read the translation instructions from: {output_dir}/02-prompt.md
You are translating chunk {NN} of {total_chunks}.
Context: {brief description of what this chunk covers and where it sits in the overall argument}
Translate this chunk:
1. Read `{output_dir}/chunks/chunk-{NN}.md`
2. Translate following the instructions in 02-prompt.md
3. Save translation to `{output_dir}/chunks/chunk-{NN}-draft.md`
```
### Non-chunked mode
```
Read the translation instructions from: {output_dir}/02-prompt.md
Translate the source file and save the result:
1. Read `{source_file_path}`
2. Save translation to `{output_path}`
```
references/workflow-mechanics.md
# Workflow Mechanics
## Materialize Source
| Input Type | Action |
|------------|--------|
| File | Use as-is (no copy needed) |
| Inline text | Save to `translate/{slug}.md` |
| URL | Fetch content, save to `translate/{slug}.md` |
`{slug}`: 2-4 word kebab-case slug derived from content topic.
## Create Output Directory
Subdirectory next to the source: `{source-dir}/{source-basename}-{target-lang}/` (e.g., `posts/article.md` → `posts/article-zh/`)
## Conflict Resolution
If the output directory exists, rename it to `{name}.backup-YYYYMMDD-HHMMSS/` before creating the new one. Never overwrite existing results.
scripts/bun.lock
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "baoyu-translate-chunk",
"dependencies": {
"markdown-it": "14.1.1",
},
},
},
"packages": {
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
}
}
scripts/chunk.ts
import { mkdirSync, readFileSync, writeFileSync } from "fs"
import { dirname, join } from "path"
import MarkdownIt from "markdown-it"
type BlockKind =
| "heading"
| "thematicBreak"
| "html"
| "code"
| "flow"
interface Block {
kind: BlockKind
md: string
words: number
}
interface Chunk {
blocks: Block[]
words: number
}
export interface ChunkCliOptions {
file: string
maxWords: number
outputDir: string
}
export interface ChunkResult {
source: string
chunks: number
output_dir: string
frontmatter: boolean
words_per_chunk: number[]
}
const parser = new MarkdownIt({ html: true })
export function formatChunkUsage(command: string): string {
return `Usage: ${command} <file> [--max-words 5000] [--output-dir <dir>]`
}
export function runChunkCli(args: string[], command = "chunk.ts"): number {
const parsed = parseChunkCliArgs(args)
if ("help" in parsed) {
console.log(formatChunkUsage(command))
return 0
}
if ("error" in parsed) {
console.error(parsed.error)
console.error(formatChunkUsage(command))
return 1
}
const result = chunkMarkdownFile(parsed.file, {
maxWords: parsed.maxWords,
outputDir: parsed.outputDir,
})
console.log(JSON.stringify(result))
return 0
}
export function chunkMarkdownFile(
file: string,
options: { maxWords?: number; outputDir?: string } = {}
): ChunkResult {
const maxWords = options.maxWords ?? 5000
const outputDir = options.outputDir ?? ""
const rawContent = normalizeNewlines(readFileSync(file, "utf-8"))
const { frontmatter, body } = extractFrontmatter(rawContent)
const chunks = buildChunks(parseMarkdown(body), maxWords)
const dir = outputDir ? join(outputDir, "chunks") : join(dirname(file), "chunks")
mkdirSync(dir, { recursive: true })
if (frontmatter) {
writeFileSync(join(dir, "frontmatter.md"), frontmatter)
}
chunks.forEach((chunk, index) => {
const num = String(index + 1).padStart(2, "0")
writeFileSync(join(dir, `chunk-${num}.md`), chunk.blocks.map(block => block.md).join("\n\n"))
})
return {
source: file,
chunks: chunks.length,
output_dir: dir,
frontmatter: Boolean(frontmatter),
words_per_chunk: chunks.map(chunk => chunk.words),
}
}
function parseChunkCliArgs(args: string[]):
| ChunkCliOptions
| { help: true }
| { error: string } {
let file = ""
let maxWords = 5000
let outputDir = ""
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (arg === "-h" || arg === "--help") {
return { help: true }
}
if (arg === "--max-words") {
const value = args[index + 1]
if (!value) return { error: "Missing value for --max-words" }
maxWords = parsePositiveInt(value, 0)
if (maxWords <= 0) return { error: `Invalid --max-words value: ${value}` }
index += 1
continue
}
if (arg === "--output-dir") {
const value = args[index + 1]
if (!value) return { error: "Missing value for --output-dir" }
outputDir = value
index += 1
continue
}
if (arg.startsWith("-")) {
return { error: `Unknown option: ${arg}` }
}
if (!file) {
file = arg
continue
}
return { error: `Unexpected positional argument: ${arg}` }
}
if (!file) {
return { error: "Missing input file" }
}
return { file, maxWords, outputDir }
}
function parsePositiveInt(value: string | undefined, fallback: number): number {
if (!value) return fallback
const parsed = Number.parseInt(value, 10)
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
}
function normalizeNewlines(text: string): string {
return text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n")
}
function trimBoundaryBlankLines(text: string): string {
return text.replace(/^\n+/, "").replace(/\n+$/, "")
}
function extractFrontmatter(content: string): { frontmatter: string; body: string } {
const lines = content.split("\n")
if (lines[0] !== "---") {
return { frontmatter: "", body: content }
}
for (let index = 1; index < lines.length; index += 1) {
if (lines[index] === "---" || lines[index] === "...") {
return {
frontmatter: lines.slice(0, index + 1).join("\n"),
body: lines.slice(index + 1).join("\n").replace(/^\n+/, ""),
}
}
}
return { frontmatter: "", body: content }
}
function parseMarkdown(content: string): Block[] {
if (!content.trim()) return []
const lines = content.split("\n")
const tokens = parser.parse(content, {})
const blocks: Block[] = []
for (const token of tokens) {
if (!token.map || token.level !== 0) continue
if (token.nesting !== 1 && token.nesting !== 0) continue
const [startLine, endLine] = token.map
const md = trimBoundaryBlankLines(lines.slice(startLine, endLine).join("\n"))
if (!md) continue
blocks.push(makeBlock(tokenTypeToBlockKind(token.type), md))
}
if (blocks.length === 0) {
const body = trimBoundaryBlankLines(content)
if (body) {
blocks.push(makeBlock("flow", body))
}
}
return blocks
}
function tokenTypeToBlockKind(tokenType: string): BlockKind {
if (tokenType === "heading_open") return "heading"
if (tokenType === "hr") return "thematicBreak"
if (tokenType === "html_block") return "html"
if (tokenType === "fence" || tokenType === "code_block") return "code"
return "flow"
}
function makeBlock(kind: BlockKind, md: string): Block {
return {
kind,
md: trimBoundaryBlankLines(md),
words: countWords(md),
}
}
function buildChunks(blocks: Block[], maxWordsPerChunk: number): Chunk[] {
const sections = splitIntoSections(blocks)
const normalizedBlocks: Block[] = []
for (const section of sections) {
const sectionWords = section.reduce((sum, block) => sum + block.words, 0)
if (sectionWords <= maxWordsPerChunk) {
normalizedBlocks.push(makeBlock("flow", section.map(block => block.md).join("\n\n")))
continue
}
for (const block of section) {
normalizedBlocks.push(...splitOversizedBlock(block, maxWordsPerChunk))
}
}
const chunks: Chunk[] = []
let currentBlocks: Block[] = []
let currentWords = 0
for (const block of normalizedBlocks) {
if (currentWords + block.words > maxWordsPerChunk && currentBlocks.length > 0) {
chunks.push({ blocks: currentBlocks, words: currentWords })
currentBlocks = [block]
currentWords = block.words
continue
}
currentBlocks.push(block)
currentWords += block.words
}
if (currentBlocks.length > 0) {
chunks.push({ blocks: currentBlocks, words: currentWords })
}
return chunks
}
function splitIntoSections(blocks: Block[]): Block[][] {
const sections: Block[][] = []
let current: Block[] = []
for (const block of blocks) {
if (block.kind === "heading" && current.length > 0) {
sections.push(current)
current = [block]
continue
}
current.push(block)
}
if (current.length > 0) {
sections.push(current)
}
return sections
}
function splitOversizedBlock(block: Block, maxWordsPerChunk: number): Block[] {
if (block.words <= maxWordsPerChunk) return [block]
if (
block.kind === "heading"
|| block.kind === "thematicBreak"
|| block.kind === "html"
|| block.kind === "code"
) {
return [block]
}
const lines = block.md.split("\n")
if (lines.length <= 1) {
return [block]
}
const splitBlocks: Block[] = []
let buffer: string[] = []
let bufferWords = 0
for (const line of lines) {
const lineWords = countWords(line)
if (bufferWords + lineWords > maxWordsPerChunk && buffer.length > 0) {
splitBlocks.push(makeBlock(block.kind, buffer.join("\n")))
buffer = [line]
bufferWords = lineWords
continue
}
buffer.push(line)
bufferWords += lineWords
}
if (buffer.length > 0) {
splitBlocks.push(makeBlock(block.kind, buffer.join("\n")))
}
return splitBlocks
}
function countWords(text: string): number {
const cleaned = text.replace(/[#*`\[\]()>|_~-]/g, " ")
const cjk = cleaned.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g)
const latin = cleaned.match(/[a-zA-Z0-9]+/g)
return (cjk?.length ?? 0) + (latin?.length ?? 0)
}
if (import.meta.main) {
process.exit(runChunkCli(process.argv.slice(2), process.argv[1] ?? "chunk.ts"))
}
scripts/main.ts
#!/usr/bin/env bun
import path from "node:path"
import process from "node:process"
import { runChunkCli } from "./chunk.js"
function formatScriptCommand(fallback: string): string {
const raw = process.argv[1]
const displayPath = raw
? (() => {
const relative = path.relative(process.cwd(), raw)
return relative && !relative.startsWith("..") ? relative : raw
})()
: fallback
const quotedPath = displayPath.includes(" ")
? `"${displayPath.replace(/"/g, '\\"')}"`
: displayPath
return `npx -y bun ${quotedPath}`
}
function printUsage(exitCode: number): never {
const cmd = formatScriptCommand("scripts/main.ts")
console.log(`Baoyu Translate CLI
Usage:
${cmd} <file> [--max-words 5000] [--output-dir <dir>]
${cmd} chunk <file> [--max-words 5000] [--output-dir <dir>]
Commands:
chunk Split markdown into chunks
Options:
--max-words <n> Maximum words per chunk (default: 5000)
--output-dir <dir> Write chunks into <dir>/chunks/
-h, --help Show help
`)
process.exit(exitCode)
}
const args = process.argv.slice(2)
if (args.length === 0) {
printUsage(1)
}
if (args[0] === "-h" || args[0] === "--help") {
printUsage(0)
}
if (args[0] === "chunk") {
process.exit(runChunkCli(args.slice(1), `${formatScriptCommand("scripts/main.ts")} chunk`))
}
process.exit(runChunkCli(args, formatScriptCommand("scripts/main.ts")))
scripts/package.json
{
"name": "baoyu-translate-chunk",
"private": true,
"dependencies": {
"markdown-it": "14.1.1"
}
}
SKILL.md
---
name: baoyu-translate
description: >-
This skill should be used when the user asks to "translate", "翻译", "精翻", "translate article",
"translate to Chinese", "translate to English", "改成中文", "改成英文", "convert to Chinese",
"localize", "本地化", "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻",
"这篇文章翻译一下", or provides a URL/file with translation intent. Supports three modes
(quick/normal/refined) with custom glossary support.
version: 1.117.3
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-translate
requires:
anyBins:
- bun
- npx
---
# Translator
## User Input Tools
When this skill prompts the user, follow this tool-selection rule (priority order):
1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question.
3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order.
Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes.
## Script Directory
`{baseDir}` = this SKILL.md's directory. `${BUN_X}` = `bun` if installed; else `npx -y bun` if `npx` available; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | CLI entry point. Default action splits markdown into chunks; also supports explicit `chunk` subcommand |
| `scripts/chunk.ts` | Markdown chunking implementation used by `main.ts` and kept compatible for direct invocation |
## Preferences (EXTEND.md)
First one found wins:
| Priority | Path | Scope |
|----------|------|-------|
| 1 | `.baoyu-skills/baoyu-translate/EXTEND.md` | Project |
| 2 | `${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-translate/EXTEND.md` | XDG |
| 3 | `$HOME/.baoyu-skills/baoyu-translate/EXTEND.md` | User home |
- **Found** → read and apply. On first use in a session, remind briefly: "Using preferences from [path]. You can edit EXTEND.md to customize glossary, audience, etc."
- **Not found** → **BLOCKING** first-time setup before any translation; never silently use defaults. Ask all questions in [references/config/first-time-setup.md](references/config/first-time-setup.md) (target language, mode, audience, style, save location) in ONE call, write EXTEND.md at the chosen location, confirm "Preferences saved to [path]", then continue.
Supports: target language, mode, audience, glossaries (inline or file path), style, chunk settings. Schema: [references/config/extend-schema.md](references/config/extend-schema.md).
## Defaults
EXTEND.md overrides defaults; CLI flags override EXTEND.md.
| Setting | Default | EXTEND.md key | CLI flag |
|---------|---------|---------------|----------|
| Target language | `zh-CN` | `target_language` | `--to` |
| Mode | `normal` | `default_mode` | `--mode` |
| Audience | `general` | `audience` | `--audience` |
| Style | `storytelling` | `style` | `--style` |
| Chunk threshold (words, triggers chunking) | `4000` | `chunk_threshold` | — |
| Chunk max words | `5000` | `chunk_max_words` | — |
## Modes
| Mode | Flag | Steps | When to Use |
|------|------|-------|-------------|
| Quick | `--mode quick` | Translate | Short texts, informal content, quick tasks |
| Normal | `--mode normal` (default) | Analyze → Translate | Articles, blog posts, general content |
| Refined | `--mode refined` | Analyze → Translate → Review → Polish | Publication-quality, important documents |
Auto-detection: "快翻", "quick", "直接翻译" → quick; "精翻", "refined", "publication quality", "proofread" → refined; otherwise the default mode.
**Style presets** (voice and tone, independent of audience; custom descriptions accepted, e.g. `--style "poetic and lyrical"`):
| Value | Description | Effect |
|-------|-------------|--------|
| `storytelling` | Engaging narrative flow (default) | Draws readers in, smooth transitions, vivid phrasing |
| `formal` | Professional, structured | Neutral tone, clear organization, no colloquialisms |
| `technical` | Precise, documentation-style | Concise, terminology-heavy, minimal embellishment |
| `literal` | Close to original structure | Minimal restructuring, preserves source sentence patterns |
| `academic` | Scholarly, rigorous | Formal register, complex clauses OK, citation-aware |
| `business` | Concise, results-focused | Action-oriented, executive-friendly, bullet-point mindset |
| `humorous` | Preserves and adapts humor | Witty, playful, recreates comedic effect in target language |
| `conversational` | Casual, spoken-like | Friendly, approachable, as if explaining to a friend |
| `elegant` | Literary, polished prose | Aesthetically refined, rhythmic, carefully crafted word choices |
**Audience presets** (custom descriptions accepted, e.g. `--audience "AI感兴趣的普通读者"`):
| Value | Description | Effect |
|-------|-------------|--------|
| `general` | General readers (default) | Plain language, more translator's notes for jargon |
| `technical` | Developers / engineers | Less annotation on common tech terms |
| `academic` | Researchers / scholars | Formal register, precise terminology |
| `business` | Business professionals | Business-friendly tone, explain tech concepts |
## Workflow
### Step 1: Load Preferences
1. Check EXTEND.md (see Preferences)
2. Load the built-in glossary for the language pair if available — EN→ZH: [references/glossary-en-zh.md](references/glossary-en-zh.md)
3. Merge glossaries: EXTEND.md `glossary` (inline) + `glossary_files` (paths relative to EXTEND.md) + built-in + `--glossary` file (CLI overrides all)
### Step 2: Materialize Source & Create Output Directory
Materialize the source (file as-is; inline text/URL → `translate/{slug}.md`), then create `{source-dir}/{source-basename}-{target-lang}/`. Detect source language if `--from` is not given. Details and conflict handling: [references/workflow-mechanics.md](references/workflow-mechanics.md).
All intermediate and final files go in the output directory:
| File | Mode | Description |
|------|------|-------------|
| `translation.md` | All | Final translation (always this name) |
| `01-analysis.md` | Normal, Refined | Content analysis (domain, tone, terminology) |
| `02-prompt.md` | Normal, Refined | Assembled translation prompt |
| `03-draft.md` | Refined | Initial draft before review |
| `04-critique.md` | Refined | Critical review findings (diagnosis only) |
| `05-revision.md` | Refined | Revised translation based on critique |
| `chunks/` | Chunked | Source chunks + translated chunks |
### Step 3: Assess Content Length
**Quick mode never chunks.** If content exceeds the chunk threshold, warn: "This article is ~{N} words. Quick mode translates in one pass without chunking — for long content, `--mode normal` produces better results with terminology consistency." Proceed if the user doesn't switch.
Normal/refined: below threshold → translate as a single unit; at or above → chunked translation (3.1).
**3.1 Long Content Preparation** (normal/refined, ≥ threshold)
1. **Extract terminology** across the whole document (proper nouns, technical terms, recurring phrases)
2. **Build session glossary**: merge extracted terms with loaded glossaries, fix consistent translations
3. **Split**: `${BUN_X} {baseDir}/scripts/main.ts <file> [--max-words <chunk_max_words>] [--output-dir <output-dir>]` — splits at markdown block boundaries (headings, paragraphs, lists, code blocks, tables, etc.); an oversized single block falls back to line, then word splitting
4. **Assemble prompt**: read `01-analysis.md` (if it exists) and build shared context with Part 1 of [references/subagent-prompt-template.md](references/subagent-prompt-template.md), inlining target style, content background, merged glossary, and translation challenges. Save as `02-prompt.md` (shared context only, no task instructions)
5. **Draft via subagents** (if an Agent tool is available):
- One subagent **per chunk**, all in parallel (Part 2 of the template). Each reads `02-prompt.md`, gets its position (chunk N of M + brief context of where it sits in the argument), and saves `chunks/chunk-NN-draft.md`
- Consistency comes from the shared `02-prompt.md` (glossary, figurative language mapping, comprehension challenges, source voice, translation challenges)
- No chunks (under threshold): one subagent for the whole source file
- No Agent tool: translate chunks sequentially inline using `02-prompt.md`
6. **Merge** chunks in order after all subagents finish; prepend `chunks/frontmatter.md` if it exists. Save as `03-draft.md` (refined) or `translation.md` (normal)
7. Keep all intermediate files in `chunks/`
After merging, the main agent takes over for review, revision, and polish (Step 4).
### Step 4: Translate & Refine
**Translation principles** (all modes):
- **Rewrite, not translate**: write natural, engaging target language as if a skilled native writer composed it. Test: "Does this read like it was originally written in the target language?"
- **Accuracy first**: facts, data, and logic match the original exactly
- **Natural flow**: idiomatic target word order; break long source sentences into shorter ones; render metaphors and idioms by intended meaning, not word-for-word
- **Terminology**: standard translations, used consistently; annotate specialized terms with the original in parentheses on first occurrence
- **Preserve format**: all markdown formatting (headings, bold, italic, images, links, code blocks)
- **Proactive interpretation**: for jargon or concepts the audience may lack context for, add concise explanations in **bold parentheses** `(**解释**)` — few, only where genuinely needed
- **Frontmatter** (if the source has YAML frontmatter): rename source-metadata fields with a `source` prefix (camelCase: `url`→`sourceUrl`, `title`→`sourceTitle`, etc.), add translated values as new top-level fields (skip `title` if the body has an H1), keep other fields as-is
**Quick**: translate directly → `translation.md`.
**Normal**:
1. Analyze → `01-analysis.md` (domain, tone, terminology, translation challenges)
2. Assemble prompt → `02-prompt.md` (instructions with context, glossary, challenges)
3. Translate following `02-prompt.md` → `translation.md`
Then tell the user: "Translation saved. To further review and polish, reply **继续润色** or **refine**." If they do, run refined Steps 4-6 on the existing output: rename current `translation.md` to `03-draft.md`, then write `04-critique.md`, `05-revision.md`, and an updated `translation.md`.
**Refined** — per-step guidelines in [references/refined-workflow.md](references/refined-workflow.md). Each step reads the previous step's file:
1. Analyze → `01-analysis.md`
2. Assemble prompt → `02-prompt.md` (instructions with inlined context)
3. Draft → `03-draft.md` (with translator's notes; from subagents if chunked)
4. Critical review → `04-critique.md` (diagnosis only: accuracy, Europeanized language, strategy execution, expression issues)
5. Revision → `05-revision.md` (apply all critique findings)
6. Polish → `translation.md` (publication quality)
Subagents (Step 3.1) only produce the initial draft. Review, revision, and polish belong to the main agent, which may delegate to subagents at its discretion.
### Step 5: Output
Final translation is always `translation.md` in the output directory.
Then run a lightweight image-language pass: collect image references, identify likely text-heavy ones (covers, screenshots, diagrams, charts, frameworks, infographics), and list any whose main text language likely differs from the translated article. List only — do not localize images unless the user asks. Use the article's own image syntax (markdown or wikilink):
```text
Possible image localization needed:
- : likely still contains source-language text while the article is now in target language
- : likely text-heavy framework graphic, check whether labels need translation
```
Summary:
```
**Translation complete** ({mode} mode)
Source: {source-path}
Languages: {from} → {to}
Output dir: {output-dir}/
Final: {output-dir}/translation.md
Glossary terms applied: {count}
```
If candidates were found, append after the summary a short note that some embedded images may still need image-text localization, followed by the list.
## Extension Support
Custom configurations via EXTEND.md — see **Preferences** above.