폰트와 스크립트 — 임베딩 라이선스, 로케일 스크립트 커버리지, 두부(tofu) 검증 증거, RTL 셰이핑, 서브셋 안전성. 트리거 "폰트 커버리지", "폰트 라이선스", "두부", "tofu", "글리프", "스크립트 커버리지", "RTL", "서브셋".
Scanned 9/19/2026
Install to Claude Code
npx -y skills add snwlee/Nereus --skill typeface --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Typeface?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/snwlee-typeface)More formats (shields.io, HTML) on the badges page.
---
name: typeface
description: 폰트와 스크립트 — 임베딩 라이선스, 로케일 스크립트 커버리지, 두부(tofu) 검증 증거, RTL 셰이핑, 서브셋 안전성. 트리거 "폰트 커버리지", "폰트 라이선스", "두부", "tofu", "글리프", "스크립트 커버리지", "RTL", "서브셋".
---
# typeface
nereus:common 규칙을 따른다. 담당 에이전트: l10n-engineer.
## 0. 진짜 제약은 번역이 아니라 폰트다
도너(19 로케일 실운영, 읽기 전용)에서 나온 실측이다:
- **Oswald/Rubik/Didot 은 한국어·일본어·태국어를 빈 네모로 그린다.**
- **렌더러는 빠진 글리프를 `.notdef` 로 그리고 `exit 0` 한다.**
로케일 하나가 통째로 두부로 나가는데 **모든 게이트가 초록이다.**
- RTL 에서 두 톤 타이틀이 문자열을 쪼개면 셰이퍼가 공백을 잃고 순서를 뒤집는다.
- **아랍 우선 폰트에는 라틴 숫자가 없다** — "4K" 가 아랍어 옆에서 두부가 됐다.
라틴·아랍을 같이 덮는 얼굴이 필요하다.
- CJK 는 "Bold" 요청이 **SemiBold 에 이름 매칭**되어 한국어만 얇게 나왔다.
19 에서 이미 이랬다. **86 이면 스크립트 커버리지가 지배적 제약이다** —
RTL 만 7종(`ar`·`ur`·`iw-IL`·`fa` 4변종)이고 인도계·동남아 스크립트가 크게 는다.
**폰트를 찾기 전에 폰트 트리 전체를 `find` 한다.** 도너가 "없다"고 결론낸 얼굴들이
전부 있었고, 잘못된 경로를 본 것이었다.
## 1. 하네스는 폰트를 파싱하지 않는다 — 증거를 요구한다
이유 둘:
1. 폰트 파싱은 새 런타임 의존성을 부른다.
2. **하네스가 파싱해도 그 사고를 못 막는다.** 실제 렌더러가 고른 얼굴과 다를 수 있고,
사고는 "폰트에 글리프가 없다"가 아니라 **"렌더러가 두부를 그리고 성공했다"** 였다.
검증은 **렌더 직후 그 자리에서** 해야 의미가 있다.
그래서 프로젝트가 증거를 제출한다:
```json
"render": {
"ko-KR": { "glyphCheck": { "ranAt": "2026-09-13", "tool": "fonttools", "missing": [] }, "titleRuns": 1 }
}
```
| 위반 코드 | 뜻 |
|---|---|
| `tofu-unverified` | 렌더 대상인데 증거가 없다. **미설정을 통과로 읽지 않는다** |
| `glyph-missing` | 증거에 빠진 글리프가 있다. 그대로 렌더하면 두부로 발행된다 |
| `rtl-split-run` | RTL 인데 타이틀을 여러 조각으로 그린다 |
RTL 판정은 `locales.json` 의 `direction` 으로만 한다 — 코드에 목록을 박지 않는다.
`render` 를 주지 않은 로케일은 렌더 대상이 아니라 두부 검사를 하지 않는다.
## 2. 폰트 검사
```bash
echo '{"requiredEmbedding":"game","typography":{"maxFontKb":900,"minSizePx":18},"fonts":[…],"targetLocales":["ko-KR"]}' \
| node "${CLAUDE_PLUGIN_ROOT}/lib/font-check.mjs"
```
폰트 선언 형태:
```json
{ "name": "GameSans", "embedding": ["game"], "scripts": ["latin", "hangul"], "sizeKb": 400, "minSizePx": 18, "subset": false }
```
| 위반 코드 | 뜻 |
|---|---|
| `required-embedding-undeclared` | 요구 임베딩 종류를 안 줬다. **건너뛰지 않는다** |
| `license-embedding` | 폰트가 그 임베딩을 선언하지 않았다 |
| `script-uncovered` | 대상 로케일이 요구하는 스크립트를 어떤 폰트도 안 덮는다 |
| `subset-unsafe` | 유저 생성 텍스트가 있는데 서브셋했다 |
| `font-size-budget` | 폰트 용량 합이 예산을 넘는다 |
| `min-size` | 최소 표시 크기가 하한 미만이다 |
### 요구 임베딩은 제품이 정한다
게임은 `"game"`, 앱은 그 제품의 라이선스가 요구하는 종류다. **코드에 박지 않는다.**
**선언이 없으면 허용으로 치지 않는다.** 상당수 폰트가 특정 임베딩을 금지하거나 별도
계약을 요구한다. 웹폰트 라이선스로 게임에 넣는 것은 위반이다. **모름은 허용이 아니다.**
### 예산 기준은 값으로 받는다
`typography: { maxFontKb, minSizePx }`. 장르 프로파일은 게임 고유라 이 모듈이 전제하지 않는다
(게임은 `nereus-game:localization` 이 `profile.typography` 를 꺼내 넘긴다).
기준이 없으면 예산·크기는 `unmeasured` 에 남고 **통과로 내지 않는다.**
라이선스·스크립트·서브셋은 기준과 무관하므로 계속 판정된다.
### 유저 생성 텍스트가 있으면 서브셋을 금지한다
한글 11,172자를 다 넣으면 폰트가 수 MB 라 서브셋이 표준 해법이다. 그런데 닉네임·채팅은
**어떤 글자가 올지 모른다** — 서브셋한 폰트에서 유저 이름이 두부(□□□)가 된다.
용량 최적화의 대가가 그거면 최적화가 아니다.
## 3. 폭 판정은 폰트 메트릭이 있을 때만 실측이다
`avgCharWidth` 는 `expansion` 과 **다른 축**이다. 한글은 번역하면 글자 수가 줄지만
(`expansion` 0.8) 글자당 폭은 전각이라 라틴의 두 배다(`avgCharWidth` 1.0). 섞지 않는다.
메트릭을 주지 않으면 확장률 근사이고, 위반에 **`approx: true`** 가 붙는다.
## 4. 하지 말 것
- **글리프 증거 미제출을 통과로 읽지 않는다.** 성공처럼 보이는 것이 정확히 그 사고였다.
- **RTL 타이틀을 색 입히려고 쪼개지 않는다.** 두 톤 타이틀은 올바른 텍스트의 대가다.
- **요구 임베딩 기본값을 박지 않는다.** 제품이 정한다.
- **폰트 파일을 파싱하지 않는다.** 새 런타임 의존성을 추가하지 않는다.
- **스크립트가 없다고 결론내기 전에 폰트 트리 전체를 `find` 한다.**
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!