# KOROMAN - Korean Romanizer

**KOROMAN** is a multilingual Romanizer for Korean text, based on the Revised Romanization system (국립국어원 표기법) with additional pronunciation rules. It converts Hangul syllables into Romanized Latin script across multiple languages: **JavaScript, Python, and Java**.

**KOROMAN**은 한국어 텍스트를 **로마자 표기법(국립국어원 표준)**에 따라 로마자로 변환해주는 다국어 라이브러리입니다. 자바스크립트, 파이썬, 자바 환경에서 모두 사용할 수 있으며, 실제 발음에 가까운 표기를 위해 **발음 규칙**도 적용할 수 있습니다.

## 🌐 Live Demo
- [한국어 버전](https://daissue.app/romanizer)
- [English version](https://daissue.app/en/romanizer)

---

## 📦 Features
- Supports Revised Romanization of Korean
- Applies key Korean phonological rules:
  - Liaison (연음화)
  - Nasal assimilation (비음화)
  - Lateralization (유음화)
  - Aspiration / consonant cluster simplification — refined in 1.0.15
- Casing options (lower, upper, capitalized) — accepts full names, short aliases, or numeric codes (1.0.14+)
- **Optional hyphen at ambiguous syllable boundaries** (`useHyphen`, 1.0.15+) — e.g. 중앙 → `jung-ang`, 반구대 → `ban-gudae`, 해운대 → `hae-undae`
- **User dictionary** (`customDictionary`, 1.0.15+) — per-call or persistent module-level store; match Korean words and emit user-provided Romanization with the highest priority (casing/phonological rules are not applied to dictionary values)
- ESM / CJS / TypeScript support
- Fully tested in each language

## 📦 주요 기능
- 표준 로마자 표기법 기반 변환 (국립국어원)
- 발음 규칙 적용 지원:
  - 연음화 (예: 해돋이 → haedoji)
  - 비음화, 유음화, 거센소리되기, 자음군 단순화 — 1.0.15에서 규칙 다듬음
- 대소문자 옵션 지원 (소문자, 대문자, 단어/줄 단위 대문자 등) — 1.0.14부터 짧은 별칭/숫자 코드도 허용
- **모호 음절 경계 자동 붙임표** (`useHyphen`, 1.0.15+) — 예: 중앙 → `jung-ang`, 해운대 → `hae-undae`
- **사용자 사전** (`customDictionary`, 1.0.15+) — 호출별 또는 모듈 내부 영구 저장소. 한국어 단어를 사용자 지정 표기로 최우선 치환 (사전 값에는 casing/음운 규칙 미적용)
- 자바스크립트 ESM/CJS/TS 대응
- 각 언어별 테스트 코드 포함

---

## 🆕 1.0.16 changes

- **29항 n-insertion (`-잎` trigger)**: `꽃잎`→`kkonnip`, etc.
- **Cluster + vowel liaison**, **`밟-`**, **ㄷ+ㅎ+ㅣ→치**
- `customDictionary` store starts **empty** — see [CHANGELOG.md](../CHANGELOG.md)

## 1.0.15 changes (heads-up)

- **Aspiration policy changed.** Receiver-final ㄱ/ㄷ/ㅂ + initial ㅎ is now romanized as a single fortis (`k`/`t`/`p`/`ch`) consistently. The previous noun-only exception (e.g. 묵호 → `Mukho`, 집현전 → `Jiphyeonjeon`) required morphological analysis we cannot perform reliably, so it has been dropped:
  - `묵호 → muko`, `집현전 → jipyeonjeon`, `잡혀 → japyeo`, `놓다 → nota`
- **Consonant cluster simplification (자음군 단순화) fixed**: 흙→`heuk`, 닭→`dak`, 삶→`sam`, 읊→`eup`, 값→`gap`.
- **ᆶ + ᄋ liaison preserved**: 잃어→`ireo` (previously `ileo`).
- **Final ㄷ / ㅎ** map to `t` in standard mode (previously `d` / `h`). The legacy mapping is kept for `usePronunciationRules: false` to preserve 1.0.14 behaviour.

---

## Browser demo (local)

`demo/index.html` loads `../dist/koroman.browser.js` — minimal smoke test without npm in the page.

```bash
npm run build
npx --yes serve . -p 8787
# open http://localhost:8787/demo/
```

---

## 🚀 Getting Started

### JavaScript (jsDelivr)
```html
<script src="https://cdn.jsdelivr.net/gh/gerosyab/koroman@js-v1.0.16/js/dist/koroman.browser.js"></script>
<script>
  const result = koroman.romanize("안녕하세요");
  console.log(result); // → annyeonghaseyo
</script>
```

### JavaScript (Node.js)
```bash
npm install koroman
```

#### CommonJS
```js
const koroman = require('koroman');

// Basic usage
koroman.romanize("한글"); // → "hangeul"

// With pronunciation rules disabled
koroman.romanize("해돋이", { usePronunciationRules: false }); // → "haedodi"

// With pronunciation rules enabled (default)
koroman.romanize("해돋이"); // → "haedoji"

// Casing options (full names)
koroman.romanize("한글", { casingOption: "uppercase" }); // → "HANGEUL"
koroman.romanize("안녕 한글", { casingOption: "capitalize-word" }); // → "Annyeong Hangeul"
koroman.romanize("안녕\n한글 로마자 변환", { casingOption: "capitalize-line" }); // → "Annyeong\nHangeul romaja byeonhwan"

// 1.0.14+ : short aliases / numeric codes are also accepted
koroman.romanize("한글", { casingOption: "u" });   // → "HANGEUL"
koroman.romanize("한글", { casingOption: "uc" });  // → "HANGEUL"
koroman.romanize("한글", { casingOption: 1 });     // → "HANGEUL"
koroman.romanize("안녕 한글", { casingOption: "cw" }); // → "Annyeong Hangeul"
koroman.romanize("안녕\n한글 로마자 변환", { casingOption: 2 }); // → "Annyeong\nHangeul romaja byeonhwan"

// 1.0.15+ : useHyphen — insert '-' at ambiguous syllable boundaries
koroman.romanize("중앙",   { useHyphen: true }); // → "jung-ang"
koroman.romanize("반구대", { useHyphen: true }); // → "ban-gudae"
koroman.romanize("해운대", { useHyphen: true }); // → "hae-undae"

// 1.0.15+ : customDictionary — per-call user dictionary (highest priority, casing-safe)
koroman.romanize("나는 김철수입니다", {
  customDictionary: { "김철수": "Kim Chul-soo" }
}); // → "naneun Kim Chul-sooimnida"

// 1.0.15+ : persistent dictionary (module-level store)
koroman.setCustomDictionary({ "김철수": "Kim Chul-soo", "서울": "Seoul" });
koroman.addCustomDictionary("부산", "Busan");
koroman.romanize("서울에 사는 김철수"); // → "Seoule saneun Kim Chul-soo"
koroman.romanize("부산", { useCustomDictionary: false }); // → "busan"
koroman.clearCustomDictionary();
```

### casingOption aliases (1.0.14+)

| Canonical          | Aliases                              | Numeric   |
|--------------------|--------------------------------------|-----------|
| `lowercase`        | `lower`, `l`, `lc`                   | `0`       |
| `uppercase`        | `upper`, `u`, `uc`                   | `1`       |
| `capitalize-line`  | `cap-line`, `cline`, `cl`            | `2`       |
| `capitalize-word`  | `cap-word`, `cword`, `cw`            | `3`       |

Case-insensitive. Unknown / `null` / `undefined` falls back to `lowercase`.

### Persistent customDictionary API (1.0.15+)

| API | Description |
|---|---|
| `setCustomDictionary(dict?)` | Replace the entire store. `null`/empty object clears it. |
| `addCustomDictionary(dict)` / `addCustomDictionary(key, value)` | Merge/insert entries. |
| `removeCustomDictionaryEntry(key)` | Remove a single key (returns `true` on success). |
| `clearCustomDictionary()` | Empty the store. |
| `getCustomDictionary()` | Shallow snapshot of the current store. |

Combine with the per-call `customDictionary` option to override the store on a single call (same-key entries in the option win). Set `useCustomDictionary: false` on a call to ignore the persistent store entirely.

#### ESM (requires `"type": "module"` in package.json)
```js
import { romanize } from 'koroman';

romanize("한글"); // → "hangeul"
```

#### TypeScript
```ts
import { romanize } from 'koroman';

const result: string = romanize("로마자", {
  usePronunciationRules: true,
  casingOption: "capitalize-line"
}); // → "Romaja"
```
---

## 📜 LICENSE
[MIT License](LICENSE)

2025 ⓒ Donghe Youn (Daissue)
