diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..2043de7 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "ko-javascript-info-tools-local", + "interface": { + "displayName": "Ko Javascript Info Tools Local" + }, + "plugins": [ + { + "name": "ko-javascript-info-tools-codex", + "source": { + "source": "local", + "path": "./plugins/ko-javascript-info-tools-codex" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/README.md b/README.md index 55cfd08..76082b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ko-javascript-info-tools -[모던 JavaScript 튜토리얼](https://javascript.info/) 한국어 번역 프로젝트 [ko.javascript.info](https://github.com/javascript-tutorial/ko.javascript.info)의 번역 품질을 검증하는 Claude Code 플러그인입니다. +[모던 JavaScript 튜토리얼](https://javascript.info/) 한국어 번역 프로젝트 [ko.javascript.info](https://github.com/javascript-tutorial/ko.javascript.info)의 번역 품질을 검증하는 Claude Code 및 Codex용 도구 모음입니다. 번역 파일(`.md`)을 지정하면 5개의 에이전트가 병렬로 규칙을 검사하고, 마크다운 보고서와 JSON 파일을 생성합니다. @@ -11,11 +11,12 @@ ### 사전 요구사항 - [Claude Code](https://claude.ai/code) CLI 설치 +- [Codex](https://developers.openai.com/codex/) CLI 또는 앱 설치 - Python 3 (맞춤법 검사 에이전트에 필요) - Node.js + [hanspell](https://www.npmjs.com/package/hanspell) (`npm install -g hanspell`) - macOS / Linux / WSL -### 플러그인 설치 +### Claude Code 플러그인 설치 Claude Code에서 아래 명령을 순서대로 실행합니다. @@ -25,6 +26,17 @@ Claude Code에서 아래 명령을 순서대로 실행합니다. /reload-plugins ``` +### Codex 플러그인 설치 + +저장소를 클론한 뒤 Codex 로컬 마켓플레이스와 플러그인을 등록합니다. + +``` +codex plugin marketplace add /path/to/ko-javascript-info-tools +codex plugin add ko-javascript-info-tools-codex@ko-javascript-info-tools-local +``` + +설치 또는 업데이트 후 새 태스크에서 스킬을 사용합니다. + ### 업데이트 ``` @@ -37,6 +49,8 @@ Claude Code에서 아래 명령을 순서대로 실행합니다. ## 사용법 +### Claude Code + ``` /translation-validator <번역 파일 경로> ``` @@ -47,6 +61,32 @@ Claude Code에서 아래 명령을 순서대로 실행합니다. /translation-validator 1-js/02-first-steps/03-strict-mode/article.md ``` +### Codex + +Codex에서는 Claude의 `/translation-validator`와 같은 전체 검증 프로세스를 실행할 수 있습니다. + +아래 예시는 **터미널 명령이 아니라 Codex 채팅창에 입력하는 프롬프트**입니다. + +``` +Use $javascriptinfo-ko-translation-validator to review 1-js/02-first-steps/03-strict-mode/article.md +``` + +Codex는 WIKI, KIGO, CUSTOM 검증을 각각 독립 에이전트로 병렬 실행하고, 메인 에이전트에서 맞춤법 검사를 동시에 실행합니다. 이후 결과를 병합해 `<파일명>_validation.json`을 저장하고, 선택에 따라 안전한 항목을 수정한 뒤 전체 검증을 다시 실행합니다. + +맞춤법만 검사하려면 `$korean-spell-checker`를 사용합니다. + +아래 예시는 **터미널 명령이 아니라 Codex 채팅창에 입력하는 프롬프트**입니다. + +``` +Use $korean-spell-checker to check 1-js/02-first-steps/03-strict-mode/article.md +``` + +터미널에서 직접 실행하려면 스킬에 포함된 스크립트를 실행합니다. + +``` +python3 ~/.codex/skills/korean-spell-checker/scripts/check_spelling.py 1-js/02-first-steps/03-strict-mode/article.md +``` + --- ## 에이전트 구성 @@ -263,6 +303,9 @@ article.md → article_validation.json ``` . +├── .agents/ +│ └── plugins/ +│ └── marketplace.json # Codex 로컬 마켓플레이스 정보 ├── .claude-plugin/ │ ├── plugin.json # 플러그인 메타데이터 │ └── marketplace.json # 마켓플레이스 정보 @@ -283,10 +326,27 @@ article.md → article_validation.json │ ├── _text_utils.py # 마크다운 전처리 공유 모듈 │ ├── check_spelling.py # 맞춤법 검사 스크립트 │ └── check_glossary.py # 용어집 일관성 검사 스크립트 -└── glossary/ +├── glossary/ ├── meta.json # 용어집 시트 메타·해시 ├── sheet1.csv # 일반 기술 용어 캐시 └── sheet2.csv # 기호·구두점 표기 캐시 +└── plugins/ + └── ko-javascript-info-tools-codex/ # Codex 전용 플러그인 + ├── .codex-plugin/ + │ └── plugin.json # Codex 플러그인 메타데이터 + └── skills/ + ├── javascriptinfo-ko-translation-validator/ + │ ├── SKILL.md # Codex 번역 검증 스킬 정의 + │ ├── agents/openai.yaml + │ └── references/ + │ ├── wiki-guidelines.md + │ ├── kigo-guidelines.md + │ └── custom-rules.md + └── korean-spell-checker/ + ├── SKILL.md # Codex 맞춤법 검사 스킬 정의 + ├── agents/openai.yaml + ├── scripts/check_spelling.py + └── tests/test_check_spelling.py ``` --- diff --git a/plugins/ko-javascript-info-tools-codex/.codex-plugin/plugin.json b/plugins/ko-javascript-info-tools-codex/.codex-plugin/plugin.json new file mode 100644 index 0000000..272cca0 --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/.codex-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "ko-javascript-info-tools-codex", + "version": "1.0.0", + "description": "Codex tools for reviewing ko.javascript.info Korean translations, including spelling checks and translation validation skills.", + "author": { + "name": "Hyun Woo Park", + "email": "gusdn3477@gmail.com", + "url": "https://github.com/gusdn3477" + }, + "repository": "https://github.com/BHyeonKim/ko-javascript-info-tools", + "keywords": [ + "korean", + "translation", + "javascript-info", + "spell-check" + ], + "skills": "./skills/", + "interface": { + "displayName": "ko.javascript.info Tools", + "shortDescription": "Korean translation review tools for Codex", + "longDescription": "Review ko.javascript.info Korean translation files with Codex skills for spelling checks and project translation rules.", + "developerName": "Hyun Woo Park", + "category": "Productivity", + "capabilities": [ + "Read", + "Write", + "Interactive" + ], + "defaultPrompt": [ + "Check this Korean Markdown translation for spelling.", + "Review this ko.javascript.info article translation." + ], + "brandColor": "#F59E0B", + "screenshots": [] + } +} diff --git a/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/SKILL.md b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/SKILL.md new file mode 100644 index 0000000..6c0837b --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/SKILL.md @@ -0,0 +1,180 @@ +--- +name: javascriptinfo-ko-translation-validator +description: > + ko.javascript.info 한국어 Markdown 번역을 WIKI 번역 모범 사례, KIGO IT 스타일, + 프로젝트 커스텀 규칙, 한국어 맞춤법 기준으로 검증하고 선택적으로 수정한다. + "번역 검토해줘", "번역 확인해줘", "번역 검증해줘", "번역 피드백", + "번역 규칙 맞는지 봐줘", "번역 검사해줘" 요청이나 번역된 .md 파일이 + 포함된 PR 검토, 검증 JSON 저장, 검증 결과에 따른 안전한 수정 요청에 사용한다. +--- + +# ko.javascript.info 번역 검증 + +Claude `translation-validator`와 같은 네 가지 검사를 실행하고, 결과를 병합해 한국어 보고서와 JSON으로 저장한 뒤 선택에 따라 안전한 항목을 수정한다. + +## 검증 절차 + +### 1단계 — 대상 파일 결정 + +1. 대상 경로를 절대 경로로 변환한다. +2. 파일 경로이면 해당 파일을 검증한다. +3. 디렉터리 경로이면 `rg --files <디렉터리> | rg '\.md$'`로 Markdown 파일을 정렬해 찾고 파일별로 전체 절차를 순차 실행한다. +4. 대상이 없으면 `검토할 번역 문서의 파일 또는 디렉터리 경로를 알려 주세요.`라고 질문한다. +5. 대상 파일 전체와 다음 참조 문서를 읽는다. + - [wiki-guidelines.md](references/wiki-guidelines.md) + - [kigo-guidelines.md](references/kigo-guidelines.md) + - [custom-rules.md](references/custom-rules.md) + +참조 문서와 스크립트 경로는 이 `SKILL.md` 위치를 기준으로 계산한다. 현재 작업 디렉터리가 스킬 디렉터리라고 가정하지 않는다. + +### 2단계 — 네 검사 병렬 실행 + +WIKI, KIGO, CUSTOM, SPELL 검사는 서로 독립적으로 실행한다. 협업 도구가 있으면 WIKI·KIGO·CUSTOM 하위 에이전트 세 개를 동시에 시작하고 메인 에이전트에서 SPELL을 실행한다. 협업 도구가 없으면 메인 에이전트에서 세 규칙을 구분해 차례대로 검사한다. + +각 규칙 하위 에이전트에는 다음 정보만 전달한다. + +- 검증 대상 절대 경로 +- 담당 참조 문서 절대 경로 하나 +- 담당 규칙 접두사와 `source` +- 아래 JSON 형식 +- 공통 제외 범위와 줄 번호 규칙 + +각 하위 에이전트는 대상과 참조 문서를 읽고 JSON만 반환한다. + +```json +{ + "source": "wiki | kigo | custom", + "violations": [ + { + "line": 1, + "rule_id": "WIKI-N | KIGO-N | CUSTOM-N", + "problem": "원문의 정확한 위반 문자열 또는 위반 내용", + "suggestion": "정확한 대체 문자열 또는 수동 수정 제안", + "severity": "required | recommended | info" + } + ], + "passed": ["통과한 규칙 항목"] +} +``` + +담당 범위는 다음과 같다. + +- **WIKI**: `wiki-guidelines.md`의 모든 `WIKI-*` 규칙, `source: "wiki"` +- **KIGO**: `kigo-guidelines.md`의 모든 `KIGO-*` 규칙, `source: "kigo"` +- **CUSTOM**: `custom-rules.md`의 모든 `CUSTOM-*` 규칙, `source: "custom"` + +SPELL은 같은 플러그인에 포함된 Codex 맞춤법 스킬의 기본 `hanspell` 백엔드로 실행한다. + +```bash +python3 <이-스킬-디렉터리>/../korean-spell-checker/scripts/check_spelling.py <대상-절대경로> +``` + +맞춤법 결과의 `line`, `problem`, `suggestion`, `explanation`을 보존하고 각 항목을 `rule_id: "SPELL"`, `source: "spell"`, `severity: "required"`로 정규화한다. 스크립트가 없거나 실패하면 오류 원문을 보존해 SPELL을 생략 처리한다. 결과를 임의로 만들거나 생략한 검사를 통과로 표시하지 않는다. + +### 3단계 — 결과 검증 및 병합 + +모든 검사에 다음 기준을 적용한다. + +- 위반 사항은 정확한 1부터 시작하는 원문 줄 번호를 기록한다. 맞춤법 백엔드가 위치를 찾지 못한 경우에만 `null`을 사용한다. +- 코드 블록, 인라인 코드, URL, Markdown 이미지 대상, HTML 태그, 원문 인용은 언어 규칙 검사에서 제외한다. +- Markdown 헤딩은 `WIKI-15` 검사 대상에 포함한다. +- `CUSTOM-병기`는 파일 전체에서 용어의 첫 등장만 검사한다. +- 명백한 위반과 맞춤법 오류는 `required`, 맥락에 따른 스타일 개선은 `recommended`, 사람의 판단이 필요한 의견은 `info`로 분류한다. +- 안전한 직접 치환은 `problem`에 원문의 정확한 문자열, `suggestion`에 정확한 대체 문자열만 넣는다. 문장 재작성이나 맥락 판단이 필요하면 설명형 제안으로 작성해 수동 수정 대상으로 구분한다. + +하위 에이전트 응답이 유효한 JSON인지 필수 필드가 모두 있는지 확인한다. 잘못된 응답은 한 번만 수정 요청하고, 다시 실패하면 정확한 사유와 함께 해당 검사를 생략한다. + +동일한 위반만 중복 제거하고 줄 번호(`null`은 마지막), 규칙 ID 순서로 정렬한다. 병합된 목록에서 심각도별 건수를 다시 계산한다. + +### 4단계 — 보고서 출력 및 JSON 저장 + +파일별로 다음 형식의 한국어 보고서를 출력한다. + +```markdown +## 번역 검토 결과: `파일경로` + +### 위반 사항 (N개) + +| 줄 | 규칙 ID | 심각도 | 위반 내용 | 수정 제안 | +|---:|---|---|---|---| + +### 통과·생략 항목 +- WIKI: ... +- KIGO: ... +- CUSTOM: ... +- SPELL: ... + +### 총평 +필수·권고·참고 항목 수와 우선순위를 간략히 설명합니다. +``` + +심각도는 `🔴 필수`, `🟡 권고`, `⚪ 참고`로 표시하고 표 셀을 깨뜨리는 문자는 이스케이프한다. + +보고서 출력 직후 대상 파일 옆에 `<원본파일명>_validation.json`을 저장하고 절대 경로를 출력한다. 검증 JSON 저장은 검증 절차의 일부이며 번역 원문 수정 권한을 뜻하지 않는다. + +```json +{ + "meta": { + "validated_file": "/absolute/path/to/article.md", + "validated_at": "2026-05-09T10:22:55Z", + "summary": { + "total": 0, + "required": 0, + "recommended": 0, + "info": 0 + } + }, + "violations": [ + { + "line": 1, + "rule_id": "WIKI-N", + "source": "wiki", + "problem": "위반 내용", + "suggestion": "수정 제안", + "severity": "required" + } + ], + "passed": { + "wiki": [], + "kigo": [], + "custom": [], + "spell": [] + }, + "omitted": {} +} +``` + +UTC ISO 8601 타임스탬프를 사용한다. 네 검사가 모두 완료되면 `omitted` 키를 생략하고, 그렇지 않으면 생략한 `source`와 오류 원문을 기록한다. + +### 5단계 — 자동 수정 선택 + +모든 대상의 보고서와 JSON 저장이 끝나면 다음 선택지를 제시한다. + +1. **필수 항목만 수정** — 맞춤법과 명백한 필수 위반 중 안전한 항목만 적용 +2. **필수 + 권고 항목 수정** — 안전한 권고 항목까지 적용 +3. **건너뛰기** — 번역 원문을 변경하지 않음 + +사용자가 1번이나 2번을 선택하기 전에는 번역 원문을 수정하지 않는다. 검증 요청만으로 수정 권한을 추정하지 않는다. + +수정 권한을 받으면 다음 절차를 따른다. + +1. 원문과 저장된 JSON을 다시 읽는다. +2. 각 위반을 자동 적용, 수동 수정 필요, 건너뜀으로 분류한다. +3. 제외 범위 밖에서 `problem`이 유일하게 일치하고 치환이 명확한 항목만 한 번에 하나씩 수정한 뒤 문맥을 다시 확인한다. +4. 같은 줄의 여러 수정은 뒤쪽 항목부터 적용한다. +5. 문장 재작성, 중복 일치, `info`, 기술적 의미를 바꿀 가능성이 있는 변경은 수동 제안으로 남긴다. +6. 적용·수동 수정 필요·건너뜀 결과를 별도 표로 보고한다. +7. 수정된 파일에 네 검사를 다시 실행한다. +8. JSON의 결과, 통과·생략 정보, 타임스탬프, 통계를 재검증 결과로 교체하고 `meta.auto_fix`에 수정 기록을 남긴다. + +```json +{ + "applied_at": "", + "mode": "required_only | required_and_recommended", + "applied": 0, + "skipped": 0, + "manual_required": 0 +} +``` + +`applied`는 실제로 파일에 반영한 고유 치환 횟수로 센다. 같은 치환으로 여러 규칙의 중복 위반이 해결돼도 한 건으로 계산한다. 파일 변경과 수정 문맥을 확인하지 않은 항목은 적용했다고 보고하지 않는다. diff --git a/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/agents/openai.yaml b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/agents/openai.yaml new file mode 100644 index 0000000..6f778ca --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "ko.javascript.info 번역 검증" + short_description: "4개 검사를 병렬 실행해 한국어 번역 품질을 검증합니다" + default_prompt: "$javascriptinfo-ko-translation-validator로 한국어 번역 문서를 검토해 주세요." diff --git a/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/custom-rules.md b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/custom-rules.md new file mode 100644 index 0000000..024afdc --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/custom-rules.md @@ -0,0 +1,50 @@ +# ko.javascript.info 프로젝트 커스텀 규칙 + +프로젝트에서 별도로 정한 추가 규칙. + +## CUSTOM-병기 한-영 병기 + +주제에서 새롭게 등장하는 키워드는 **첫 등장 시** 한-영 병기한다. + +형식: `한국어(영어)` 또는 `한국어(영어 약어, 영어 풀이)` + +예시: +- `프로퍼티(property)` +- `브라우저 객체 모델(Browser Object Model, BOM)` +- `이벤트 루프(event loop)` +- `콜 스택(call stack)` + +**적용 범위**: 해당 파일 내 **첫 등장**에만 적용. 이후 반복 등장 시 한국어만 사용해도 됨. + +**확인 방법**: 파일 전체를 읽어 해당 용어의 첫 등장 위치를 확인한다. + +**예외**: 코드 블록, 인라인 코드(`` ` ``) 내부의 영어 식별자는 병기 대상 아님. + +## CUSTOM-옮긴이 번역자 부가설명 + +원문에는 없으나 독자의 이해를 돕기 위해 번역자가 추가하는 내용은 아래 형식으로만 삽입한다. + +``` +문장 중간: 본문 내용(...부가설명... - 옮긴이) 이어지는 내용 +문장 끝: 본문 내용. (...부가설명... - 옮긴이) +``` + +- ✅ `클로저(closure)는 함수와 그 함수가 선언된 렉시컬 환경(JavaScript에서 함수가 정의된 시점의 스코프를 기억하는 메커니즘 - 옮긴이)의 조합입니다.` +- ❌ `클로저(closure)는 함수와 그 함수가 선언된 렉시컬 환경의 조합입니다. [역주: JavaScript에서...]` + +**확인 포인트**: `- 옮긴이` 표기 없이 번역자 의견이 삽입된 경우 위반. + +## CUSTOM-금지표현 적∙의 표현 금지 + +'적(的)', '의(義)를 보이는' 것∙들에 대한 표현은 사용하지 않는다. +구체적으로: `-적인`, `-적으로` 형태의 한자어 조어를 가급적 피하고 순우리말로 바꾼다. + +참고 링크: https://github.com/javascript-tutorial/ko.javascript.info/wiki/번역-모범-사례 (링크 내 해당 항목) + +예시: +- ❌ `효율적인 방법` → ✅ `효율 좋은 방법` / `효율이 높은 방법` +- ❌ `기본적으로` → ✅ `기본으로` / `기본상` +- ❌ `일반적인 경우` → ✅ `보통의 경우` / `흔한 경우` +- ❌ `구체적인 예시` → ✅ `구체적 예시` (명사 앞 `-인` 제거) 또는 `실제 예시` + +**심각도**: 권고(🟡) — 완전히 금지는 아니나 가급적 피할 것. diff --git a/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/kigo-guidelines.md b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/kigo-guidelines.md new file mode 100644 index 0000000..079e7dc --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/kigo-guidelines.md @@ -0,0 +1,150 @@ +# KIGO 번역 스타일 가이드 — IT 문서 핵심 규칙 + +출처: KIGO 표준 스타일 가이드 v2017 (한국 IT 산업세계화학회) +javascript.info 번역에 관련 있는 항목만 발췌. + +## KIGO-시제 + +미래 시제(`~할 것입니다`, `~있을 것입니다`)는 현재형으로 대체한다. + +- ❌ `찾을 수 있을 것입니다` → ✅ `찾을 수 있습니다` +- ❌ `사용할 것입니다` → ✅ `사용합니다` + +## KIGO-콜론 + +세미콜론(`;`)은 쉼표(`,`) 또는 마침표(`.`)로 대체한다. + +영문 소스가 콜론(`:`)으로 끝나면 번역 시 마침표(`.`)로 대체한다. +단, 영문 소스가 구(phrase) 형태인 경우에만 콜론 유지. + +- ❌ `이 기능은 지원되지 않습니다;` → ✅ `이 기능은 지원되지 않습니다.` +- ✅ (구 형태) `서버 목록:` + +## KIGO-괄호 + +구 형태의 괄호 삽입: 괄호 내 마침표 없음. + +- ✅ `이 데이터를 업데이트하세요(업데이트 안내서 참조).` + +완전한 문장의 괄호 삽입: 괄호 내 마침표 있음. + +- ✅ `일련 번호를 입력합니다(기호는 이 표에서 제거되었습니다.)` + +## KIGO-조사 + +괄호 다음의 조사는 괄호 **앞** 단어 기준으로 결정한다. + +- ✅ `첨부 파일(계약서)을` (파일 → 을) +- ✅ `SMTP(Simple Mail Transfer Protocol)를` (SMTP → 를) + +변수(`{%}`) 뒤에는 이중 조사 사용: `은(는)`, `이(가)`, `을(를)`, `과(와)` + +## KIGO-약어 + +약어는 대문자로 표기하며, 복수 `s`는 붙이지 않는다. + +- ✅ `PDA(Personal Data Assistant)` (PDAs → PDA) +- ✅ `FTP` (Ftp → FTP) + +IT 분야 표기 순서: `약어(한글 번역)` + +- ✅ `USB(범용직렬버스)` +- ✅ `KIGO(한국 IT 산업세계화학회)` + +## KIGO-외래어 + +IT 관련 주요 외래어 표기 규칙: + +- `ㅈ, ㅊ`은 단모음으로 표기: `버전`(버젼 ❌), `프로시저`(프로시져 ❌), `차트`(챠트 ❌) +- 단모음 뒤 어말 무성 파열음 받침 처리: `인터넷`, `서브셋`, `템플릿`, `툴킷` +- 관용 예외: `세트`, `네트`, `키트` +- 장모음 표기 없음: `팀`(team), `섀도`(shadow) + +## KIGO-경어 + +기본적으로 경어를 사용한다. + +| 형식 | 일반 | 경어(표준) | +|------|------|------------| +| 평서문 | ~한다 | ~합니다 | +| 명령문 | ~해라 | ~하십시오 / ~하세요 | +| 의문문 | ~할까? | ~할까요? | + +한 문장에 여러 동사가 있으면 **맨 끝에만** 높임말 사용. + +- ❌ `구매하실 때에는 사용하시기 바랍니다` +- ✅ `구매할 때에는 사용하시기 바랍니다` + +단계별 사용법 설명에서는 명령형보다 **평서형**을 권장한다. + +- `속성을 선택하세요.` → 권장: `속성을 선택합니다.` + +## KIGO-주체생략 + +주체가 분명할 경우 생략한다. + +- ❌ `사용자는 USB 카드 목록도 확인할 수 있습니다.` +- ✅ `USB 카드 목록도 확인할 수 있습니다.` + +단, 서로 다른 주체가 한 문장에 있으면 명확히 구별한다. + +## KIGO-조사남용 + +불필요한 조사를 삭제하고, 격조사 `~의`를 연이어 사용하지 않는다. + +- ❌ `당사의 제품의 버전에` → ✅ `당사의 제품 버전에` +- ❌ `시스템의 폴더의 이름을` → ✅ `시스템의 폴더 이름을` + +## KIGO-수동형 + +수동형(`~되어지다`, `~불려지다`)을 능동형으로 바꾼다. + +- ❌ `프로그램이 설치된 후` → ✅ `프로그램을 설치한 후` +- ❌ `불려집니다` → ✅ `합니다` + +## KIGO-복수형 + +복수형을 가급적 단수형으로 표현한다. + +- ❌ `항목들을 삭제할 수 있습니다` → ✅ `항목을 삭제할 수 있습니다` + +## KIGO-맞춤법 + +자주 틀리는 단어: + +| 틀린 표기 | 올바른 표기 | +|-----------|------------| +| 예/아니오 | 예/아니요 | +| 그리고 나서 | 그러고 나서 | +| 몇일 | 며칠 | +| 하므로써 | 함으로써 | +| 찾아 보다 | 찾아보다 | +| 도와 주다 | 도와주다 | +| 삼가하다 | 삼가다 | + +쓰임새가 혼동되는 표현: + +| 표현 | 경우 1 | 경우 2 | +|------|--------|--------| +| 하는데 vs 하는 데 | 집에 오는데 그가 불렀다 | 사용하는 데 문제가 없다 | +| 로써 vs 로서 | 사용함으로써 성공했다 | 사용자로서 기분이 좋다 | +| 든지 vs 던지 | 하든지 말든지 | 얼마나 피곤했던지 | + +## KIGO-제목 + +제목(heading)은 서술형보다 간결한 명사형을 사용한다. `-하기` 형태도 피한다. +단, `만들기`, `보기` 등 고유어는 `-하기` 형태 허용. + +- ❌ `설정을 테스트합니다.` / `설정 테스트하기` +- ✅ `설정 테스트` + +## KIGO-제품명 + +회사 이름, 제품 이름, 소프트웨어 이름은 번역하지 않는다. 특히 등록상표(®, ™)가 붙은 단어는 절대 번역하지 않는다. + +- ❌ `정글 보안은 보안 회사입니다.` +- ✅ `Jungle Security는 보안 회사입니다.` + +## KIGO-저작권 + +저작권 및 상표 관련 텍스트(`Copyright © ... All Rights Reserved`)는 번역하지 않는다. diff --git a/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/wiki-guidelines.md b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/wiki-guidelines.md new file mode 100644 index 0000000..2f0c409 --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/javascriptinfo-ko-translation-validator/references/wiki-guidelines.md @@ -0,0 +1,123 @@ +# ko.javascript.info 위키 번역 모범 사례 + +출처: https://github.com/javascript-tutorial/ko.javascript.info/wiki/번역-모범-사례 + +## WIKI-1 쌍점(콜론) 금지 + +문장 끝에 콜론(`:`) 사용 금지. 코드 예시나 목록을 도입할 때도 문장을 마침표로 끝낸다. + +- ❌ `다음 예제를 보세요:` +- ✅ `다음 예제를 보세요.` + +예외: 구(phrase) 형태인 경우 콜론 유지 (예: `서버 목록:`) + +## WIKI-2 '-시-' 제한 + +과도한 존칭 '-시-' 사용 금지. 청자 존중 표현은 문말 경어로 충분하다. + +- ❌ `살펴보시면 좋습니다` +- ✅ `살펴보면 좋습니다` / `살펴보세요` + +## WIKI-3 쉼표 선별 사용 + +영어 쉼표를 한국어에 기계적으로 옮기지 않는다. + +- ❌ `예를 들어, id가 행의 ID일 경우, 다음 코드가 작동합니다.` +- ✅ `예를 들어 id가 행의 ID일 경우 다음 코드가 작동합니다.` + +## WIKI-4 동사 번역 + +외래어 동사는 "명사형 + 하다" 형태로 번역한다. + +- `render` → `렌더링하다` +- `bind` → `바인딩하다` +- `fetch` → `가져오다` (의미 번역 허용) + +## WIKI-5 인칭대명사 생략 + +"당신", "여러분", "우리" 등의 인칭대명사를 생략해도 자연스러우면 생략한다. + +- ❌ `여러분이 DOM 엘리먼트에 표현한다고 생각해봅시다` +- ✅ `DOM 엘리먼트에 표현한다고 생각해봅시다` + +## WIKI-6 피동형 최소화 + +불필요한 피동형 표현을 능동형으로 바꾼다. + +- ❌ `화면에 보여지는 엘리먼트` +- ✅ `화면에 보이는 엘리먼트` +- ❌ `가상 네트워크라고 불려집니다` +- ✅ `가상 네트워크라고 합니다` + +## WIKI-7 표현 다양성 + +같은 단어를 반복하지 않는다. `common`을 예로 들면: + +- `일반적으로`, `널리`, `흔히` 중 문맥에 맞게 선택 + +## WIKI-8 단수형 선호 + +복수형 `-들`은 필수적인 경우에만 사용한다. + +- ❌ `아래 예시들은 차이점들을 보여줍니다` +- ✅ `아래 예시는 차이점을 보여줍니다` + +## WIKI-9 '만약' 생략 + +`~한다면` 어미 자체가 조건을 표현하므로 "만약"을 반드시 붙일 필요 없다. + +- ❌ `만약 값이 없다면` +- ✅ `값이 없다면` + +## WIKI-10 괄호 문장 형식 + +- 구(phrase) 형태: 괄호 내 마침표 없음 → `업데이트하세요(안내서 참조)` +- 완전한 문장: 괄호 내 마침표 있음 → `일련 번호를 입력합니다(기호는 이 표에서 제거되었습니다.)` +- 괄호 앞 마침표 금지: ❌ `입력합니다.(기호는...)` ✅ `입력합니다(기호는...)` + +## WIKI-11 약어 표기 + +IT 분야: 약어를 앞에, 한글 번역을 괄호로 뒤에. + +- `USB(범용직렬버스)`, `API(애플리케이션 프로그래밍 인터페이스)` + +IT 외 분야: 한글 번역을 앞에, 약어를 괄호로 뒤에. + +- `미국중재협회(AAA)` + +## WIKI-12 함수 명칭 + +함수 이름 뒤에 "함수"를 붙이는 것을 기본으로 한다. + +- ✅ `exit 함수`, `clear 함수` +- ✅ `async 함수` (범주 지칭 시 뒤에 붙임) + +## WIKI-13 강조 따옴표 + +직접 인용이 아닌 강조 표현에는 작은따옴표(`'`) 사용. + +- ✅ `잠시 '중단'되었다가` + +## WIKI-14 대명사 명확화 + +"it", "that"처럼 가리키는 대상이 모호할 때 구체적 명사로 풀어 쓴다. + +## WIKI-15 제목 문장부호 금지 + +마크다운 헤딩(#, ##, ###)에는 마침표나 물음표를 붙이지 않는다. + +- ❌ `## 배열 메서드.` +- ✅ `## 배열 메서드` + +## WIKI-16 가운뎃점 사용 + +짝을 이루는 단어는 슬래시(`/`) 대신 가운뎃점(`·`) 사용. + +- ❌ `대/소문자`, `삽입/삭제` +- ✅ `대·소문자`, `삽입·삭제` + +## WIKI-17 명사형 문장 마침표 생략 + +명사로 끝나는 설명 문장은 마침표를 붙이지 않는다. + +- ✅ `텍스트가 입력될 때마다 이벤트 발생` diff --git a/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/SKILL.md b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/SKILL.md new file mode 100644 index 0000000..9aee134 --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/SKILL.md @@ -0,0 +1,76 @@ +--- +name: korean-spell-checker +description: > + Markdown 또는 일반 텍스트의 한국어 맞춤법과 띄어쓰기를 검사한다. 특히 + ko.javascript.info 한국어 번역 문서에 사용한다. 사용자가 맞춤법, 띄어쓰기, + 오탈자 검사, hanspell 실행, .md 한국어 문서 검토, Claude 맞춤법 검사 + 워크플로의 Codex 변환을 요청할 때 사용한다. +--- + +# 한국어 맞춤법 검사 + +포함된 `scripts/check_spelling.py`로 `hanspell`을 실행하고 Markdown 또는 일반 텍스트의 한국어 맞춤법과 띄어쓰기 문제를 보고한다. + +`$korean-spell-checker`는 Codex에서 스킬을 호출하는 문법이며 셸 명령어가 아니다. 터미널에서는 Python 스크립트를 직접 실행한다. + +## 검사 절차 + +1. 검사 대상을 결정한다. + - 사용자가 파일 경로를 주면 해당 파일을 검사한다. + - 디렉터리 경로를 주면 `rg --files <디렉터리> | rg '\.md$'`로 Markdown 파일을 찾는다. + - 대상이 불분명하면 파일 또는 디렉터리 경로를 요청한다. + +2. 이 스킬 디렉터리에서 포함된 스크립트를 실행한다. + +```bash +python3 scripts/check_spelling.py path/to/article.md +``` + +현재 작업 디렉터리가 스킬 디렉터리가 아니면 `scripts/check_spelling.py`의 절대 경로를 사용한다. + +전역으로 설치한 스킬은 다음처럼 실행한다. + +```bash +python3 ~/.codex/skills/korean-spell-checker/scripts/check_spelling.py path/to/article.md +``` + +3. JSON 출력을 해석한다. + - `violations`에는 `line`, `problem`, `suggestion`, `explanation`, `severity`가 들어 있다. + - 문제가 없으면 `passed`에 `맞춤법 오류 없음`이 들어 있다. + - `filtered.masking_artifacts`는 Markdown 마스킹 때문에 비정상적으로 큰 공백이 생겨 제외한 결과 수다. + - `filtered.non_korean`은 검사할 한글이 없는 순수 ASCII 또는 코드 문법 제안을 제외한 결과 수다. + - 스크립트가 `error`를 반환하면 의존성 또는 실행 오류를 원문 그대로 보고한다. + +4. 결과를 간결한 Markdown 표로 보고한다. + +| 줄 | 규칙 ID | 문제 | 수정 제안 | 설명 | +|---:|---|---|---|---| + +규칙 ID는 `SPELL`을 사용한다. 사용자가 완화된 편집 검토를 요청하지 않았다면 스크립트가 찾은 항목을 모두 필수로 분류한다. + +## 실행 환경 + +Python 3, Node.js, `npx`가 필요하다. 스크립트는 `npx --yes hanspell -d`를 실행하므로 처음 실행할 때 `hanspell` npm 패키지를 내려받을 수 있다. + +`npx`, Node.js 또는 `hanspell`을 사용할 수 없으면 중단하고 스크립트의 정확한 오류를 보고한다. 맞춤법 검사 결과를 임의로 만들지 않는다. + +## 오탐 방지 + +- 인라인 코드에서는 백틱만 제거하고 내용을 보존한다. 따라서 `` `fetch`로 ``를 `fetch로` 형태로 검사한다. +- Markdown 링크와 이미지에서는 대상 주소를 제거하고 표시 문구를 보존한다. 따라서 `[명세서](url)에`를 `명세서에` 형태로 검사한다. +- ``는 `fetch`로 보존하고 다른 HTML 태그는 공백을 추가하지 않고 제거한다. +- 코드 블록과 주석은 줄 번호를 유지한 채 마스킹한다. +- 네 칸 이상의 공백이 포함된 백엔드 결과는 원문이 아닌 마스킹 부산물로 보고 제외한다. +- 한글이 없는 결과는 코드 문장부호나 순수 ASCII 문법이므로 제외한다. +- 원문이 보호된 Markdown 영역 밖의 수정 가능한 텍스트와 명확히 대응할 때만 제안을 적용한다. + +## 수정 지침 + +사용자가 수정을 명시적으로 요청하지 않았다면 파일을 변경하지 않는다. + +수정할 때는 다음 원칙을 따른다. + +- 코드 블록, 인라인 코드, URL, Markdown 이미지 대상, HTML 태그 밖의 텍스트만 변경한다. +- 원문이 여러 번 등장해 대상 위치가 모호하면 해당 수정을 건너뛴다. +- 한 번에 하나씩 수정하고 변경한 문맥을 다시 읽는다. +- 문장 전체를 다시 쓰거나 기술적 의미를 바꿀 수 있으면 자동 수정 대신 수동 제안으로 남긴다. diff --git a/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/agents/openai.yaml b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/agents/openai.yaml new file mode 100644 index 0000000..f9e4dca --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "한국어 맞춤법 검사" + short_description: "한국어 번역 문서의 맞춤법과 띄어쓰기 검사 도구" + default_prompt: "$korean-spell-checker로 한국어 Markdown 번역의 맞춤법과 띄어쓰기를 검사해 주세요." diff --git a/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/scripts/check_spelling.py b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/scripts/check_spelling.py new file mode 100644 index 0000000..3684e3f --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/scripts/check_spelling.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Korean spell checker for Markdown/text files using hanspell. + +The script masks Markdown/code regions before checking, runs `npx hanspell -d`, +and emits JSON that Codex can turn into a report. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path + + +def mask_text(text: str) -> str: + """Replace non-newline characters with spaces while preserving line numbers.""" + return re.sub(r"[^\n]", " ", text) + + +def mask_pattern(content: str, pattern: str, flags: int = 0) -> str: + return re.sub(pattern, lambda match: mask_text(match.group(0)), content, flags=flags) + + +def keep_group_text(match: re.Match[str]) -> str: + """Remove Markdown wrappers while preserving the visible text.""" + return match.group(1) + + +def mask_heading_marker(match: re.Match[str]) -> str: + return " " * len(match.group(1)) + + +def prepare_content(content: str) -> str: + """Prepare Markdown for spelling checks while preserving line numbers. + + Keep visible inline-code and link text so Korean particles remain attached + (`fetch`로 -> fetch로, [명세서](url)에 -> 명세서에). Only block-level + content is space-masked because offsets are unnecessary for line mapping. + """ + cleaned = content.replace("“", '"').replace("”", '"') + + cleaned = mask_pattern(cleaned, r"\A---[\s\S]*?\n---[^\n]*(?:\n|$)") + cleaned = mask_pattern(cleaned, r"```[\s\S]*?```") + cleaned = mask_pattern(cleaned, r"~~~[\s\S]*?~~~") + cleaned = mask_pattern(cleaned, r"") + cleaned = re.sub(r"!\[([^\]\n]*)\]\([^)]+\)", keep_group_text, cleaned) + cleaned = re.sub(r"\[([^\]\n]+)\]\([^)]+\)", keep_group_text, cleaned) + cleaned = re.sub(r"`([^`\n]+)`", keep_group_text, cleaned) + cleaned = re.sub(r"\n]+)>", keep_group_text, cleaned) + cleaned = mask_pattern(cleaned, r"https?://\S+") + cleaned = re.sub(r"<[^>\n]+>", "", cleaned) + cleaned = re.sub(r"^(#{1,6}\s+)", mask_heading_marker, cleaned, flags=re.MULTILINE) + cleaned = re.sub(r"^(\s{0,3}>+\s*)", mask_heading_marker, cleaned, flags=re.MULTILINE) + cleaned = re.sub(r"^(\s*[-*+]\s+)", mask_heading_marker, cleaned, flags=re.MULTILINE) + cleaned = cleaned.replace("*", "") + return cleaned + + +def is_masking_artifact(original: str, suggestion: str) -> bool: + """Detect large whitespace gaps introduced by masked Markdown regions.""" + return bool(re.search(r"[ \t]{4,}", original) or re.search(r"[ \t]{4,}", suggestion)) + + +def is_non_korean_finding(original: str) -> bool: + """Ignore hanspell suggestions that contain no Hangul to review.""" + return re.search(r"[가-힣]", original) is None + + +def parse_hanspell_output(output: str) -> list[dict[str, str]]: + """Parse hanspell stderr entries shaped like `original -> suggestion`.""" + violations: list[dict[str, str]] = [] + lines = [ + line + for line in output.strip().splitlines() + if not line.strip().startswith(("npm notice", "npm WARN")) + ] + + i = 0 + while i < len(lines): + line = lines[i].strip() + if " -> " not in line: + i += 1 + continue + + original, suggestion = line.split(" -> ", 1) + explanation_parts: list[str] = [] + i += 1 + while i < len(lines): + next_line = lines[i].strip() + if " -> " in next_line: + break + if next_line: + explanation_parts.append(next_line) + i += 1 + + violations.append( + { + "original": original.strip(), + "suggestion": suggestion.strip(), + "explanation": " ".join(explanation_parts).strip(), + } + ) + + return violations + + +def find_line(cleaned: str, original: str, start_at: int) -> tuple[int | None, int]: + if not original: + return None, start_at + + index = cleaned.find(original, start_at) + if index == -1: + index = cleaned.find(original) + if index == -1: + return None, start_at + + line = cleaned.count("\n", 0, index) + 1 + return line, index + len(original) + + +def run_hanspell(cleaned: str, timeout: int) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["npx", "--yes", "hanspell", "-d"], + input=cleaned, + capture_output=True, + text=True, + encoding="utf-8", + timeout=timeout, + check=False, + ) + + +def check_spelling(path: Path, timeout: int, include_corrected: bool) -> dict: + if not path.is_file(): + return {"error": f"File not found: {path}"} + + content = path.read_text(encoding="utf-8") + cleaned = prepare_content(content) + + try: + result = run_hanspell(cleaned, timeout) + except subprocess.TimeoutExpired: + return {"error": f"hanspell timed out after {timeout} seconds"} + except FileNotFoundError: + return {"error": "npx not found. Install Node.js first."} + + parsed = parse_hanspell_output(result.stderr) + if result.returncode != 0 and not parsed: + stderr = result.stderr.strip() + return {"error": stderr or f"hanspell failed with exit code {result.returncode}"} + + search_pos = 0 + filtered_counts = { + "masking_artifacts": 0, + "non_korean": 0, + } + violations = [] + for item in parsed: + if is_masking_artifact(item["original"], item["suggestion"]): + filtered_counts["masking_artifacts"] += 1 + continue + if is_non_korean_finding(item["original"]): + filtered_counts["non_korean"] += 1 + continue + line, search_pos = find_line(cleaned, item["original"], search_pos) + violations.append( + { + "line": line, + "rule_id": "SPELL", + "source": "spell", + "problem": item["original"], + "suggestion": item["suggestion"], + "explanation": item["explanation"], + "severity": "required", + } + ) + + payload = { + "source": "spell", + "file": str(path.resolve()), + "total": len(violations), + "violations": violations, + "passed": ["맞춤법 오류 없음"] if not violations else [], + } + filtered_counts = {key: value for key, value in filtered_counts.items() if value} + if filtered_counts: + payload["filtered"] = filtered_counts + if include_corrected: + payload["corrected_text"] = result.stdout.strip() + return payload + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check Korean spelling with hanspell.") + parser.add_argument("file", help="Markdown or text file to check") + parser.add_argument("--timeout", type=int, default=120, help="hanspell timeout in seconds") + parser.add_argument( + "--include-corrected", + action="store_true", + help="Include hanspell corrected text in JSON output", + ) + args = parser.parse_args() + + payload = check_spelling(Path(args.file), args.timeout, args.include_corrected) + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 1 if "error" in payload else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/tests/test_check_spelling.py b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/tests/test_check_spelling.py new file mode 100644 index 0000000..e0aa1a5 --- /dev/null +++ b/plugins/ko-javascript-info-tools-codex/skills/korean-spell-checker/tests/test_check_spelling.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).parents[1] / "scripts" / "check_spelling.py" +SPEC = importlib.util.spec_from_file_location("check_spelling", SCRIPT_PATH) +assert SPEC is not None and SPEC.loader is not None +CHECKER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(CHECKER) + + +class PrepareContentTests(unittest.TestCase): + def test_keeps_inline_code_attached_to_particle(self) -> None: + self.assertEqual(CHECKER.prepare_content("`fetch`로 요청합니다."), "fetch로 요청합니다.") + + def test_keeps_link_label_attached_to_particle(self) -> None: + self.assertEqual( + CHECKER.prepare_content("[명세서](https://example.com)에 따릅니다."), + "명세서에 따릅니다.", + ) + + def test_keeps_info_reference_attached_to_particle(self) -> None: + self.assertEqual(CHECKER.prepare_content("로 이동합니다."), "fetch로 이동합니다.") + + def test_removes_emphasis_without_splitting_particles(self) -> None: + self.assertEqual(CHECKER.prepare_content("**강조**는 유지합니다."), "강조는 유지합니다.") + + def test_keeps_image_label_attached_to_particle(self) -> None: + self.assertEqual( + CHECKER.prepare_content("![구조도](diagram.png)는 예시입니다."), + "구조도는 예시입니다.", + ) + + def test_masks_fenced_code_and_preserves_line_count(self) -> None: + source = "앞 문장\n```js\nconst value = '검사 제외';\n```\n뒤 문장" + cleaned = CHECKER.prepare_content(source) + self.assertEqual(cleaned.count("\n"), source.count("\n")) + self.assertNotIn("검사 제외", cleaned) + + +class ArtifactFilterTests(unittest.TestCase): + def test_filters_large_masking_gap(self) -> None: + self.assertTrue(CHECKER.is_masking_artifact("fetch 로", "fetch로")) + + def test_keeps_normal_double_space_finding(self) -> None: + self.assertFalse(CHECKER.is_masking_artifact("단어 사이", "단어 사이")) + + def test_filters_pure_ascii_code_suggestion(self) -> None: + self.assertTrue(CHECKER.is_non_korean_finding("xhr.open('POST', ...)")) + + def test_keeps_mixed_identifier_and_korean_particle(self) -> None: + self.assertFalse(CHECKER.is_non_korean_finding("fetch로")) + + +if __name__ == "__main__": + unittest.main()