@dropshot/i18n-po
v0.2.0
Published
gettext PO 카탈로그 엔진 — 파싱·패치·3-way 병합과 ICU MessageFormat 검증·미리보기
Downloads
3,690
Readme
@dropshot/i18n-po
gettext PO 카탈로그 엔진이다. 파싱·패치·3-way 병합과 ICU MessageFormat 검증·미리보기를 한다.
의존성은 @formatjs/icu-messageformat-parser 하나뿐이고, 특정 i18n 프레임워크(next-intl·i18next·Lingui 등)를 알지 못한다. .po 파일만 다룬다.
npm install @dropshot/i18n-po왜 또 하나의 PO 파서인가
gettext-parser·pofile 같은 성숙한 파서가 이미 있다. 이 패키지가 따로 있는 이유는 파싱이 목적이 아니라서다. 번역 편집기를 GitHub PR 위에서 굴리려다 보니 파서 위에 얹을 것들이 필요했다.
- 3-way 병합 — 편집 중에 base가 바뀌면 무엇이 충돌인지 가려야 한다
- ICU 분기 편집 —
{count, plural, ...}를 사람이 칸별로 고칠 수 있게 뜯고 다시 조립한다 - 원문 수정 검증 — 원문을 고칠 때 인자가 사라졌는지, 번역이 여전히 맞는지 본다
이 셋이 필요 없다면 기존 파서가 더 나은 선택이다.
API
파싱·병합
import {parsePo, mergeCatalogs} from '@dropshot/i18n-po';
const source = parsePo(fs.readFileSync('messages/ko.po', 'utf8'));
const target = parsePo(fs.readFileSync('messages/en.po', 'utf8'));
// 두 카탈로그를 키로 맞춰 한 줄씩 짝짓는다
const rows = mergeCatalogs(source, target);
// [{key, msgctxt, msgid, source, translation, fuzzy, references, description}, ...]key는 msgctxt가 있으면 `${msgctxt}.${msgid}`, 없으면 msgid다. next-intl의 네임스페이스가 msgctxt로 떨어지므로 그대로 맞는다.
패치
원문을 통째로 다시 쓰지 않고 해당 항목만 고친다. 다른 항목의 주석·순서·포맷이 보존된다.
import {patchMsgstr, setFuzzy} from '@dropshot/i18n-po';
let po = fs.readFileSync('messages/en.po', 'utf8');
po = patchMsgstr(po, {msgctxt: 'billing', msgid: '결제가 끝났어요'}, 'Payment complete');
po = setFuzzy(po, {msgctxt: 'billing', msgid: '결제가 끝났어요'}, true);3-way 병합
편집을 시작한 시점(base)과 지금의 원격(head)이 갈렸을 때, 내 편집을 어디까지 살릴 수 있는지 가린다.
import {threeWayMergeMsgstr} from '@dropshot/i18n-po';
const {applied, dropped, conflicts} = threeWayMergeMsgstr({base, head, mine});applied— 그대로 얹힌 것dropped— head에서 키가 사라져 얹을 곳이 없는 것conflicts— base 이후 head도 바뀌어 사람이 골라야 하는 것
ICU 검증
import {validateMessage, validateSourceSyntax, restoreArgFormats} from '@dropshot/i18n-po';
validateSourceSyntax('안녕 {name}'); // {ok, errors}
validateMessage('안녕 {name}', 'Hi {name}'); // 번역이 원문의 인자를 지키는지
// 번역자가 {count, number} 를 {count} 로 흘렸을 때 원문 기준으로 되살린다
restoreArgFormats('{count, number}개', '{count} items');ICU 분기 편집
{count, plural, one {...} other {...}} 같은 메시지를 칸별 편집이 가능한 형태로 뜯고 되돌린다.
import {analyzeBranches, buildBranchMessage, branchRank, hasAdvancedIcu} from '@dropshot/i18n-po';
const info = analyzeBranches('{count, plural, one {# item} other {# items}}');
// {arg: 'count', type: 'plural', branches: [{selector: 'one', message: '# item'}, ...]}
info.branches[0].message = '# thing';
buildBranchMessage(info); // 다시 ICU 문자열로
hasAdvancedIcu(msg); // 칸 편집으로 감당 못 하는 중첩인지 (true면 원문 그대로 편집)
branchRank('one'); // 표시 순서 (zero < one < two < few < many < other)미리보기
인자를 채우지 않고 "번역자가 보게 될 모양"을 그린다.
import {renderPreview, renderPreviewParts} from '@dropshot/i18n-po';
renderPreview('{count, plural, one {# item} other {# items}}', 'en');
// 문자열 하나로
renderPreviewParts('안녕 {name}');
// [{type: 'text', value: '안녕 '}, {type: 'slot', name: 'name'}]
// 슬롯을 다르게 칠하고 싶을 때파싱에 실패하면 둘 다 null을 준다 — 편집 중인 깨진 문자열에도 안전하다.
<date>포맷키</date>는 dropshot 본체의 날짜 규약이다 — 태그 이름은 date 하나이고,
포맷 키를 태그 본문에 리터럴로 넣는다(<date>date.mixed</date>). 미리보기는 이 규약을
알아보고 실제 날짜 모양으로 찍는다. 본문이 규약 키가 아니거나 비어 있으면(번역가가 지운
경우) date.numeric으로 폴백하고, 변수·중첩 태그처럼 리터럴 하나가 아니면 다른 태그와
똑같이 본문을 펼친다. date가 아닌 태그를 서식용으로 쓰고 있었다면 이 버전부터 동작이
바뀐다 — 태그 이름이 정확히 date일 때만 해당한다.
renderPreview('<date>date.mixed</date> 공개', 'ko');
// '2026년 8월 2일 공개'날짜 서식
<date> 태그가 쓰는 날짜 서식 계산이다.
import {formatDate, resolveDateFormatKey, DATE_FORMAT_KEYS} from '@dropshot/i18n-po';
formatDate(new Date(), 'date.mixed', 'ko'); // '2026년 8월 2일'
resolveDateFormatKey('date.잘못'); // 'date.numeric' — 모르는 키는 폴백
DATE_FORMAT_KEYS; // 키 목록키는 여덟 종(date.numeric·date.compact·date.mixed·date.short·datetime.mixed·
datetime.short·time.numeric·time.short)이고 같은 키가 로케일마다 다른 모양으로
찍힌다(date.short가 ko에서는 8월 2일, en에서는 08.02). 본체의 dayjs 패턴 규약을
재구현한 것이라 로케일을 모르면 ko로, 키를 모르면 date.numeric으로 떨어진다 — 둘 다
본체 resolveDateFormatKey와 같은 폴백이다. 타임존은 UTC 고정이라 실행 환경을
따라가지 않는다.
date.compact(26.08.02 / 08.02.26)만 본체에 없고 여기서 먼저 정의한 것이다. 쓰려면
본체 앱의 format 네임스페이스와 packages/shared의 DATE_FORMAT_KEY_LIST에 같은 키를
넣어야 한다 — 그전에 메시지에서 쓰면 본체가 date.numeric으로 폴백해 미리보기와 화면이
어긋난다.
CSV 왕복
번역을 스프레드시트로 내보내고 다시 받는다.
import {serializeCsv, parseCsv, parseTranslationCsv} from '@dropshot/i18n-po';라이선스
MIT
