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.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}, ...]

keymsgctxt가 있으면 `${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/sharedDATE_FORMAT_KEY_LIST에 같은 키를 넣어야 한다 — 그전에 메시지에서 쓰면 본체가 date.numeric으로 폴백해 미리보기와 화면이 어긋난다.

CSV 왕복

번역을 스프레드시트로 내보내고 다시 받는다.

import {serializeCsv, parseCsv, parseTranslationCsv} from '@dropshot/i18n-po';

라이선스

MIT