npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dropshot/i18n-po

v0.2.3

Published

gettext PO 카탈로그 엔진 — 파싱·패치·3-way 병합과 ICU MessageFormat 검증·미리보기

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와 원문(source)은 카탈로그 형식에 따라 다음처럼 읽는다. parsePo는 두 형식을 모두 지원하므로 마이그레이션 중인 카탈로그도 같은 API로 다룰 수 있다.

# legacy: 헤더에 X-Message-Key가 없다
msgctxt "billing"
msgid "aaa111"
msgstr "결제를 완료했어요"

# next-intl 4.14: 헤더에 "X-Message-Key: msgctxt\n"가 있다
msgctxt "billing.aaa111"
msgid "결제를 완료했어요"
msgstr "Payment complete"
  • legacy의 key는 msgctxt가 있으면 `${msgctxt}.${msgid}`, 없으면 msgid이고, source는 원본 카탈로그의 msgstr이다.
  • 4.14의 key는 msgctxt ?? msgid, source는 msgid이다. id와 원문이 같으면 codec이 msgctxt를 생략하므로 msgid가 논리 키를 겸한다.
  • key는 병합·목록용 논리 키다. 패치 대상은 PO의 실제 필드 조합인 raw locator {msgctxt, msgid}로 지정한다. 같은 논리 키라도 두 형식의 raw locator는 다르다.

parsePo는 인접 항목 사이에 빈 줄이 없어도 각 항목을 읽는다. 중복 키·헤더, 누락되거나 중복된 필드, 잘못된 문자열과 지원하지 않는 구문은 오류를 던진다. obsolete 항목은 제외한다. PO의 msgid_plural/msgstr[n]은 지원하지 않으며 복수형은 기존처럼 ICU MessageFormat으로 다룬다.

parsePoCatalog(content)는 {format, entries}를 반환한다. format은 실제 헤더를 읽은 'message-key' 또는 'legacy'이며 entries는 parsePo(content)와 같다. 형식을 비교해야 하는 호출부는 본문 문자열 검색 대신 이 API를 사용한다.

패치

파싱과 동일한 항목 경계·이스케이프 규칙으로, 원문을 통째로 다시 쓰지 않고 해당 항목만 고친다. 다른 항목의 주석·순서·포맷이 보존된다.

import {patchMsgstr, setFuzzy} from '@dropshot/i18n-po';

let po = fs.readFileSync('messages/en.po', 'utf8');
const locator = {msgctxt: 'billing.aaa111', msgid: '결제를 완료했어요'};
po = patchMsgstr(po, locator, 'Payment complete');
po = setFuzzy(po, locator, 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