i18n Workflow Helpers
Utilities for common i18n workflows
Beyond parsing and serialization, pofile-ts includes utilities for common i18n workflows. All helpers are tree-shakeable — only import what you need.
Comment Processing
Split multiline comments into individual lines for PO format output. Useful when extracting comments from source code:
import { splitMultilineComments } from "pofile-ts"
// Source comments often contain newlines
splitMultilineComments(["Line1\nLine2", "Line3"])
// → ["Line1", "Line2", "Line3"]
// Handles Windows/Mac line endings
splitMultilineComments(["First\r\nSecond"])
// → ["First", "Second"]
// Trims whitespace, filters empty lines
splitMultilineComments([" Line1\n\n Line2 "])
// → ["Line1", "Line2"]Default Headers
Create properly formatted PO headers with sensible defaults:
import { createDefaultHeaders } from "pofile-ts"
const headers = createDefaultHeaders({
language: "de",
generator: "my-tool"
})
// Includes: POT-Creation-Date, MIME-Version, Content-Type, etc.
// Plural-Forms is auto-generated from CLDR when language is set:
// → "Plural-Forms: nplurals=2; plural=(n != 1);"
// To use a custom Plural-Forms header:
const custom = createDefaultHeaders({
language: "de",
pluralForms: "nplurals=2; plural=(n > 1);"
})
// To omit Plural-Forms entirely:
const noPluralForms = createDefaultHeaders({
language: "de",
pluralForms: false
})Catalog Conversion
Convert between simple key-value catalogs and PO items — with full plural support:
import { catalogToItems, itemsToCatalog } from "pofile-ts"
// Simple catalog format
const catalog = {
Hello: { translation: "Hallo" },
"{count} item": {
translation: ["{count} Element", "{count} Elemente"],
pluralSource: "{count} items"
}
}
const items = catalogToItems(catalog)
const backToCatalog = itemsToCatalog(items)Message ID Generation
Generate stable, collision-resistant IDs from message content. Uses SHA-256 with Base64URL encoding — 281 trillion possibilities, practically zero collisions even at 1M messages:
import { generateMessageId, generateMessageIdSync } from "pofile-ts"
// Async (works in browsers and Node.js)
const id = await generateMessageId("Hello {name}", "greeting")
// → "Kj9xMnPq" (8-char Base64URL)
// Sync (Node.js only, faster)
const syncId = generateMessageIdSync("Hello {name}")Reference Utilities
Parse and format source file references with proper path normalization:
import { parseReference, formatReference, createReference } from "pofile-ts"
parseReference("src/App.tsx:42")
// → { file: "src/App.tsx", line: 42 }
formatReference({ file: "src/App.tsx", line: 42 })
// → "src/App.tsx:42"
// Validates relative paths, normalizes Windows backslashes
createReference("src\\components\\Button.tsx", 10)
// → { file: "src/components/Button.tsx", line: 10 }Plural Categories (CLDR)
Get CLDR plural categories for any locale — useful for building ICU messages or validating translations.
Native Intl.PluralRules
pofile-ts uses the browser's native Intl.PluralRules API for all plural handling. This means:
- Zero bundle size for plural data — the browser provides CLDR
- Always up-to-date with the browser's CLDR version
- CSP-safe — no eval or Function() needed
| Forms | Categories | Languages |
|---|---|---|
| 1 | other | Chinese, Japanese, Korean, Vietnamese, Thai |
| 2 | one, other | English, German, Dutch, Swedish, Danish |
| 3 | one, many, other | French, Spanish, Portuguese, Italian, Catalan |
| 3 | one, few, other | Croatian, Serbian, Bosnian |
| 3 | zero, one, other | Latvian |
| 4 | one, two, few, other | Slovenian |
| 4 | one, few, many, other | Russian, Ukrainian, Polish, Czech, Lithuanian |
| 5 | one, two, few, many, other | Irish, Maltese |
| 6 | zero, one, two, few, many, other | Arabic, Welsh |
Usage
import { getPluralCategories, getPluralCount, getPluralFunction } from "pofile-ts"
// Get categories for a locale
getPluralCategories("de") // → ["one", "other"]
getPluralCategories("ru") // → ["one", "few", "many", "other"]
getPluralCategories("pl") // → ["one", "few", "many", "other"]
getPluralCategories("ar") // → ["zero", "one", "two", "few", "many", "other"]
// Count of plural forms
getPluralCount("de") // → 2
getPluralCount("ru") // → 4
getPluralCount("ar") // → 6
// Get plural selector function
const selectPlural = getPluralFunction("ru")
selectPlural(1) // → 0 (one)
selectPlural(2) // → 1 (few)
selectPlural(5) // → 2 (many)
selectPlural(21) // → 0 (one) — CLDR-correct!
selectPlural(22) // → 1 (few)Locale Normalization
Both underscore and hyphen formats are supported:
getPluralCategories("pt_BR") // → ["one", "many", "other"]
getPluralCategories("pt-BR") // → ["one", "many", "other"]Limitations & Browser Support
Browser/Node.js Requirements:
- All modern browsers (Chrome 63+, Firefox 58+, Safari 13+, Edge 79+)
- Node.js 14+ (full ICU build required for all locales)
- Deno, Bun, and Cloudflare Workers all support
Intl.PluralRules
Locale Fallback:
Unknown locales fall back to the default CLDR root rules (["one", "other"]). This matches Intl.PluralRules behavior.
Plural-Forms Header:
When using createDefaultHeaders({ language }), the generated Plural-Forms header uses a simplified expression (n != 1) for languages with 3+ forms. This is because the exact gettext expression cannot be derived from Intl.PluralRules. The nplurals count is always correct.
For runtime plural selection, use getPluralFunction() — it uses Intl.PluralRules directly and is always CLDR-accurate.
ICU Conversion
Convert Gettext plurals to ICU MessageFormat — perfect for modern i18n libraries like Lingui or FormatJS:
import { gettextToIcu, normalizeToIcu, icuToGettextSource, parsePo } from "pofile-ts"
// Convert a single plural item
const item = {
msgid: "One item",
msgid_plural: "{count} items",
msgstr: ["Ein Artikel", "{count} Artikel"]
}
gettextToIcu(item, { locale: "de" })
// → "{count, plural, one {Ein Artikel} other {{count} Artikel}}"
// Convert an entire PO file
const po = parsePo(content)
const normalized = normalizeToIcu(po, { locale: "de" })
// All plural items now have ICU in msgstr[0]
normalized.items[0].msgstr[0]
// → "{count, plural, one {Ein Artikel} other {{count} Artikel}}"Octothorpe Expansion
By default, # is replaced with {varname} for better readability in TMS tools:
// With # in source (e.g. from Lingui)
const item = { msgstr: ["# Artikel", "# Artikel"], ... }
gettextToIcu(item, { locale: "de" })
// → "{count, plural, one {{count} Artikel} other {{count} Artikel}}"
// To preserve #, set expandOctothorpe: false
gettextToIcu(item, { locale: "de", expandOctothorpe: false })
// → "{count, plural, one {# Artikel} other {# Artikel}}"Extract from ICU
Extract msgid/msgid_plural from an ICU plural string:
icuToGettextSource("{count, plural, one {# item} other {# items}}")
// → { msgid: "{count} item", msgid_plural: "{count} items", pluralVariable: "count" }Complex Locales
Works with all CLDR locales, including complex Slavic and Arabic plurals:
// Russian (4 forms) — CLDR-correct "many" for 0, 5-19, 100...
gettextToIcu(ruItem, { locale: "ru" })
// → "{count, plural, one {файл} few {файла} many {файлов} other {файла}}"
// Polish (4 forms)
gettextToIcu(plItem, { locale: "pl" })
// → "{count, plural, one {plik} few {pliki} many {plików} other {pliki}}"
// Arabic (6 forms)
gettextToIcu(arItem, { locale: "ar" })
// → "{count, plural, zero {...} one {...} two {...} few {...} many {...} other {...}}"ICU Parser
Parse and analyze ICU MessageFormat strings. Useful for extracting variables, validating syntax, or building translation tools.
Why a Custom Parser?
The parser is optimized for bundle size and typical i18n use cases:
| Library | Gzipped |
|---|---|
| @formatjs/icu-messageformat-parser | ~9KB |
| pofile-ts ICU parser | ~2KB |
Limitations
Trade-offs for smaller bundle size:
- ICU MessageFormat v1 only — no MF2 syntax
- No source location tracking — messages are typically single-line anyway
- Styles/skeletons as opaque strings — stored but not parsed (e.g.,
::currency/EUR) - Modern JS only — no IE11 polyfills
Basic Usage
import { parseIcu, validateIcu, extractVariables } from "pofile-ts"
// Parse ICU message to AST
const result = parseIcu("{count, plural, one {# item} other {# items}}")
if (result.success) {
console.log(result.ast) // Array of AST nodes
}
// Validate syntax
const validation = validateIcu("{broken, plural, one {x}}")
console.log(validation.valid) // false
console.log(validation.errors) // [{ kind: "SYNTAX_ERROR", message: "..." }]
// Extract variable names
extractVariables("{name} has {count} messages")
// → ["name", "count"]Supported Syntax
The parser supports the full ICU MessageFormat v1 specification:
// Simple arguments
parseIcu("{name}")
// Formatted arguments
parseIcu("{n, number}")
parseIcu("{d, date, short}")
parseIcu("{t, time, medium}")
// Number/date skeletons (stored as opaque strings)
parseIcu("{n, number, ::currency/EUR}")
// Plural
parseIcu("{n, plural, offset:1 =0 {none} one {# item} other {# items}}")
// Select
parseIcu("{gender, select, male {He} female {She} other {They}}")
// Selectordinal
parseIcu("{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}")
// Tags (for rich text / JSX)
parseIcu("Click <link>here</link> to continue")
// Escaping: '' → ' and '{text}' → literal text
parseIcu("It''s {name}''s turn") // "It's {name}'s turn"Variable Analysis
Extract detailed information about variables in a message:
import { extractVariableInfo, compareVariables } from "pofile-ts"
// Get variable details including type and style
extractVariableInfo("{count, number, currency} on {date, date, short}")
// → [
// { name: "count", type: "number", style: "currency" },
// { name: "date", type: "date", style: "short" }
// ]
// Compare source and translation variables
compareVariables(
"Hello {name}, you have {count} messages",
"Hallo {userName}, du hast {count} Nachrichten"
)
// → { missing: ["name"], extra: ["userName"], isMatch: false }Detection Helpers
Quick checks without full parsing:
import { hasPlural, hasSelect, hasIcuSyntax } from "pofile-ts"
hasPlural("{n, plural, one {#} other {#}}") // true
hasSelect("{g, select, male {He} other {They}}") // true
hasIcuSyntax("{name}") // true (has argument)
hasIcuSyntax("Hello world") // false (plain text)AST Node Types
The parser produces an AST with these node types:
import { IcuNodeType } from "pofile-ts"
IcuNodeType.literal // Plain text
IcuNodeType.argument // {name}
IcuNodeType.number // {n, number}
IcuNodeType.date // {d, date}
IcuNodeType.time // {t, time}
IcuNodeType.plural // {n, plural, ...}
IcuNodeType.select // {g, select, ...}
IcuNodeType.pound // # in plural
IcuNodeType.tag // <b>...</b>Parser Options
parseIcu(message, {
// Treat tags as literal text (default: false)
ignoreTag: true,
// Allow plural/select without 'other' clause (default: true = required)
requiresOtherClause: false
})ICU Compiler
Compile ICU messages to fast JavaScript functions. Instead of parsing ICU syntax at runtime, the compiler generates optimized functions that directly produce formatted strings — 3× faster than Lingui and FormatJS.
Why Compile?
| Approach | What happens at runtime |
|---|---|
| pofile-ts | Direct function call → template literal |
| @lingui | Walk AST array → apply plural rules → build string |
| intl-messageformat | Parse ICU → create AST → interpret |
The difference is like native code vs. interpreter — we eliminate all runtime parsing and AST walking.
Single Message
import { compileIcu } from "pofile-ts"
// Compile a single ICU message to a function
const greet = compileIcu("Hello {name}!", { locale: "en" })
greet({ name: "World" }) // → "Hello World!"
// Full ICU support
const msg = compileIcu("{count, plural, one {# item} other {# items}}", { locale: "en" })
msg({ count: 1 }) // → "1 item"
msg({ count: 5 }) // → "5 items"
// Number and date formatting via Intl
const price = compileIcu("{amount, number, currency}", { locale: "de" })
price({ amount: 1234.5 }) // → "1.234,50" (German formatting)Tags (Named, Numeric, React)
Tags are treated as functions that receive children and return formatted content:
// Named tags
const msg = compileIcu("Click <link>here</link> to continue", { locale: "en" })
msg({ link: (text) => `<a href="#">${text}</a>` })
// → "Click <a href="#">here</a> to continue"
// Numeric tags (Lingui-style)
const msg2 = compileIcu("Hello <0>World</0>!", { locale: "en" })
msg2({ 0: (text) => `**${text}**` })
// → "Hello **World**!"
// React components (returns array when tag returns object)
const msg3 = compileIcu("Read our <link>terms</link>", { locale: "en" })
const result = msg3({
link: (children) => ({ type: Link, props: { to: "/terms" }, children })
})
// → ["Read our ", { type: Link, ... }, ""]
// Use with React.createElement or JSXCatalog Compilation (Runtime)
Compile an entire translation catalog at once:
import { compileCatalog, itemsToCatalog, parsePo } from "pofile-ts"
const po = parsePo(poFileContent)
const catalog = itemsToCatalog(po.items)
const compiled = compileCatalog(catalog, { locale: "de" })
// Look up by messageId (8-char hash) or msgid
compiled.format("Xk9mLp2Q", { name: "Sebastian" })
compiled.has("Xk9mLp2Q") // true
compiled.keys() // ["Xk9mLp2Q", ...]
compiled.size // 42Options:
compileCatalog(catalog, {
locale: "de", // Required: for plural rules and Intl formatting
useMessageId: true, // Use 8-char hash as key (default: true)
strict: false // Throw on parse errors (default: false)
})Static Code Generation (Build-time)
Generate JavaScript/TypeScript code at build time for zero-runtime parsing:
import { generateCompiledCode, parsePo, itemsToCatalog } from "pofile-ts"
const po = parsePo(poFileContent)
const catalog = itemsToCatalog(po.items)
const code = generateCompiledCode(catalog, {
locale: "de",
exportName: "messages"
})
// Write to file
fs.writeFileSync("compiled-de.ts", code)Generated code looks like:
/**
* Compiled messages for locale: de
* Generated by pofile-ts
*/
const _pf = (n) => (n !== 1 ? 1 : 0)
const _nf = new Intl.NumberFormat("de")
export const messages = {
Xk9mLp2Q: (v) => `Hallo ${v?.name ?? "{name}"}!`,
Ym3nPq4R: (v) =>
_pf(v?.count ?? 0) === 0
? `${_nf.format(v?.count ?? 0)} Artikel`
: `${_nf.format(v?.count ?? 0)} Artikel`
}Options:
generateCompiledCode(catalog, {
locale: "de", // Required
useMessageId: true, // Use 8-char hash as key (default: true)
exportName: "messages", // Export variable name (default: "messages")
strict: false // Throw on parse errors (default: false)
})Gettext Plural Support
The compiler handles both ICU plurals in msgstr and classic Gettext plural arrays (msgstr[]):
// ICU plural in msgstr
const catalog1 = {
Hello: { translation: "{count, plural, one {# Nachricht} other {# Nachrichten}}" }
}
// Gettext plural array (msgstr[0], msgstr[1], ...)
const catalog2 = {
"{count} message": {
translation: ["{count} Nachricht", "{count} Nachrichten"],
pluralSource: "{count} messages"
}
}
// Both compile to the same runtime behavior
const compiled1 = compileCatalog(catalog1, { locale: "de" })
const compiled2 = compileCatalog(catalog2, { locale: "de" })Performance
Benchmarked against common i18n libraries:
| Metric | pofile-ts | vs intl-messageformat | vs @lingui |
|---|---|---|---|
| Compilation | 72k ops/s | 1× same | — |
| Runtime | 810k ops/s | 3× faster | 4× faster |
| Catalog (200 msgs) | ~210k msg/s | 1× same | — |
The key insight: Lingui compiles to an AST array that's still interpreted at runtime. pofile-ts compiles to actual JavaScript functions with template literals — no interpretation needed.