`@palamedes/core`
@palamedes/core owns the app-facing i18n instance and the macro entry point package names.
@palamedes/core
Exports
The package root is the compatibility entrypoint. Its generated table below is
the canonical complete list. Runtime exports are importable values; Type-only
exports must use a TypeScript type import. Run pnpm check:core-api-reference
after building packages to verify the source, declarations, ESM, and CJS surface.
| Export | Kind |
|---|---|
buildChoiceMessage | Runtime |
CatalogMessage | Type-only |
CatalogMessages | Type-only |
ChoiceComponentProps | Type-only |
ChoiceKind | Type-only |
CompiledCatalogMessages | Type-only |
CompiledMessage | Type-only |
CompiledMessageBranch | Type-only |
CompiledMessageBranches | Type-only |
CompiledMessageRuntime | Type-only |
createCompiledMessageRuntime | Runtime |
createI18n | Runtime |
CreateI18nOptions | Type-only |
DEFAULT_LOCALE | Runtime |
defineCompiledCatalog | Runtime |
ExecutableMessageRenderer | Type-only |
formatMessageArgument | Runtime |
formatMessagePattern | Runtime |
MessageChoiceNode | Type-only |
MessageFormatErrorInfo | Type-only |
MessageFormattedArgumentNode | Type-only |
MessageLiteralNode | Type-only |
MessageMetadata | Type-only |
MessageNode | Type-only |
MessageTagNode | Type-only |
MessageTextNode | Type-only |
MessageValues | Type-only |
MessageVariableNode | Type-only |
MissingMessageInfo | Type-only |
PalamedesI18n | Type-only |
parseMessagePattern | Runtime |
PluralProps | Type-only |
replacePoundPlaceholders | Runtime |
ReportedMessageError | Type-only |
resolveChoice | Runtime |
ResolvedChoice | Type-only |
SelectOrdinalProps | Type-only |
SelectProps | Type-only |
stringifyValue | Runtime |
The parser-free @palamedes/core/compiled entrypoint exports createI18n(),
the CompiledPalamedesI18n instance type, the compiled catalog ABI and types,
and the Intl formatting helpers. Its load() accepts only
CompiledCatalogMessages. It omits formatMessagePattern(),
parseMessagePattern(), and the parser implementation. Use it when all loaded
catalogs are generated by Palamedes:
import { createI18n, type CompiledCatalogMessages } from "@palamedes/core/compiled"
declare module "*.po" {
export const messages: CompiledCatalogMessages
}Its load() rejects unbranded hand-written string catalogs. Use the package-root
factory below when runtime ICU strings are intentional.
When a parser-free instance misses a compiled catalog entry, it can return a
retained metadata.message as readable source text, but it deliberately does
not parse or interpolate that ICU pattern. This keeps the compiled entry free
of the parser. Use the package-root entry when a source fallback itself must be
formatted, and attach onMissing to measure catalog/code skew rather than
adding a default console logger.
createI18n(options?)
Creates an in-memory i18n instance. It starts with DEFAULT_LOCALE ("en").
The optional locale setting overrides that initial locale for catalog lookup,
message formatting, and telemetry before any later activate() call. Set the
optional timeZone to an IANA identifier (for example "Europe/Berlin") to
format ICU date and time arguments consistently during server rendering and
client hydration. Invalid or empty identifiers throw RangeError during
creation. Without it, the host's default time zone remains in effect. Optional
telemetry hooks receive missing-message and runtime formatting failures without
changing the source-message fallback behavior.
interface CreateI18nOptions {
locale?: string
timeZone?: string
onMissing?: (info: MissingMessageInfo) => void
onError?: (info: MessageFormatErrorInfo) => void
}import { createI18n } from "@palamedes/core"
const i18n = createI18n({
timeZone: "Europe/Berlin",
onMissing(info) {
reportMetric("palamedes.missing", info)
},
onError(info) {
captureException(info.error)
},
})
i18n.load("de", {
"Hello {name}": "Hallo {name}",
})
i18n.activate("de")
const label = i18n._("Hello {name}", { name: "Ada" })Date values and timestamps are instants. Date-only ISO strings such as
"2026-06-12" are parsed by JavaScript as UTC before Palamedes renders them in
timeZone, so they can display as the prior or following calendar day in some
zones. Pass an explicit local-time value when the input is a calendar date.
PalamedesI18n
interface PalamedesI18n {
readonly locale: string
readonly timeZone?: string
_(id: string, values?, metadata?): string
load(locale: string, messages: CatalogMessages | CompiledCatalogMessages): void
activate(locale: string): void
getMessage(id: string, metadata?: MessageMetadata): string
getMessageNodes(id: string, metadata?: MessageMetadata): MessageNode[]
parsePattern?(pattern: string): MessageNode[]
renderMessage?<TResult>(id, values, runtime, metadata?): TResult
reportError(info: ReportedMessageError): void
}parsePattern() is a parse-only adapter capability exposed by the package-root
createI18n(). Unlike getMessageNodes(), its argument is always treated as a
raw ICU pattern and never as a catalog key. It is optional so custom and older
instances remain compatible; the parser-free @palamedes/core/compiled
factory deliberately omits it. React and Solid use the capability for lazy
fallback patterns emitted by generated message functions.
getMessageNodes() remains in the shared instance type for compatibility, but
the parser-free @palamedes/core/compiled factory throws when it is called.
Use its direct compiled-message rendering path instead; code that needs parse
trees must use the package-root compatibility entrypoint.
renderMessage() executes a generated function directly against a host result
renderer and applies the same telemetry and fallback behavior as _().
First-party React and Solid adapters use this path when present, then fall back
to getMessageNodes() for older or custom instances. reportError() remains
for adapters that render outside the instance.
replacePoundPlaceholders(value, numericValue, locale?) replaces # markers
in already-resolved choice text using the instance-independent cached
Intl.NumberFormat; framework renderers use it so pound formatting shares
core's formatter cache.
getMessageNodes() returns the parsed message as a MessageNode[] tree. For
generated functions it reconstructs the ICU pattern and parses it only when
this compatibility API is called. First-party adapters render functions
directly and neither parse ICU nor allocate this tree.
A custom renderer must handle every MessageNode variant, including
MessageLiteralNode:
type MessageLiteralNode = {
type: "literal"
value: string
}Literal nodes carry text that ICU quoting escaped — the { in '{', the '
in '', the # in '#'. They render exactly like MessageTextNode; they are
a separate variant only so the parse tree records that the text was quoted. A
renderer that switches on "text", "variable", "formatted", "choice",
and "tag" alone silently drops every escaped character. See
Quoting and literal text for the quoting rules
that produce these nodes.
load() merges messages into the locale catalog. The locale passed to
createI18n({ locale }), or DEFAULT_LOCALE when omitted, is active
immediately. activate() switches the locale used by _() and getMessage().
First-party catalog loaders call defineCompiledCatalog() around a single map
during module evaluation. A value is either a constant string or a
CompiledMessage function. The non-enumerable catalog brand tells load() that
string entries are constants; function entries execute directly through Core,
React, or Solid renderers.
Spreading a generated catalog drops the constant-string brand, so copied string entries fall back to lazy parsing in the package-root factory while function entries remain executable. The parser-free factory rejects the unbranded copy. JSON is not a transport for generated catalogs because JavaScript serialization omits functions; use source PO/FCL data or a hand-written string catalog when a serializable format is required.
Fallback order for getMessage(id, metadata):
- active catalog entry for
id metadata.messageid
Because the initial locale is active immediately, onMissing also runs before
the first load() or activate() call when that locale has no matching catalog
entry. Applications that intentionally use source messages for the default
locale without loading its catalog should account for those default-locale
events in their telemetry policy.
MessageMetadata
interface MessageMetadata {
message?: string
context?: string
comment?: string
reportMissing?: boolean
}reportMissing: false suppresses onMissing for a single lookup. The runtime
choice components use it because their synthesized source patterns are
expected to miss the catalog in apps that never loaded matching entries.
The compiler emits this metadata alongside compact internal lookup ids so the
runtime can fall back to the source message and report useful diagnostics.
It is not a deferred authoring API. Author translations with t at the point
where they are evaluated.
Locale Controls
@palamedes/core/locale exposes headless locale helpers used by the example
matrix and reusable in apps:
parseAcceptLanguage(header): parses and quality-sortsAccept-Languagetags, including base-language fallbacks.buildLocaleSwitchItems(options): builds UI-agnostic switch items with labels, active state, locale, and test ids.defineLocaleControls(config): binds locale resolution, deliberate-choice cookies, canonical URLs, and suggestion decisions for cookie, route, subdomain, and tld strategies. Host-carrying URLs fromcanonicalUrlandsuggestare protocol-relative (//host/path) unless the config setsprotocol(e.g."https"), so HTTPS pages never link users tohttp://.
Macro Entry Point
Macros are imported from @palamedes/core/macro and must be compiled by a
Palamedes plugin before runtime.
Supported macro names:
tpluralselectselectOrdinal
All four macros resolve translations eagerly. They must therefore be used inside a function, method, or callback, after the application has activated the relevant i18n scope. Palamedes rejects these macros at module scope during transformation and extraction. Class field initializers, including instance fields, do not satisfy this syntactic rule; use a method or getter instead.
Runtime Formatting
formatMessagePattern() and the _() method returned by createI18n() support
the formatter subset implemented by the Palamedes runtime:
{value, number}{value, number, percent}and{value, number, integer}{value, number, ::percent},{value, number, ::integer}, and{value, number, ::currency/ISO}{value, date}and{value, time}{value, date, short|medium|long|full}{value, time, short|medium|long|full}
The standalone helpers accept the same optional final time-zone argument as the
instance runtime: formatMessagePattern(pattern, values?, locale?, timeZone?)
and formatMessageArgument(format, value, style?, locale?, timeZone?). Include
the zone in both server and client calls when these helpers contribute to
hydrated output.
Currency formatting must use the ::currency/ISO skeleton form; bare
currency/ISO is outside the supported runtime subset.
Catalog artifact compilation reports list, duration, ago, name, and
other unsupported formatter kinds as errors. Unsupported styles on number,
date, and time are warnings because the runtime currently falls back to the
default Intl formatter for that argument type.
Plural and selectordinal arguments require a present, numeric value (numeric
strings are accepted). A missing or non-numeric value throws instead of
silently coercing to 0 — inside _()/getMessage() that error is reported
through onError and rendering falls back to the source message.
Plural Offset
plural and selectordinal arguments accept ICU offset:N, where N is a
non-negative integer. It exists for "and N others" sentences, where the number
shown is smaller than the number counted:
{count, plural, offset:1 =0 {nobody else} one {# other person} other {# other people}}Three rules, applied in this order:
- Exact
=Nkeys match the raw value, before the offset is subtracted. Withoffset:1andcount = 1, the=1branch matches. - Plural categories (
zero…other) select onvalue - offset. Withoffset:1andcount = 2, the category comes from1, so English picksone. #inside the selected branch rendersvalue - offset.
A negative or non-integer offset is rejected while the pattern is parsed, so a
bad offset surfaces as a format error rather than a wrong count. select has
no numeric operand and does not support offset: an offset: written into a
select argument is read as an option key, never matches, and renders empty.
The same argument is written plural(count, { offset: 1, … }) with the macro
and <Plural offset={1}> with the React and Solid components; both compile to
the ICU form above, so the offset lives in the catalog and translators can rely
on it.
Quoting and Literal Text
The runtime parser implements ICU's apostrophe quoting in its lenient form, so
ordinary prose stays readable while {, }, and # remain escapable:
''is always a literal apostrophe:Ada''srendersAda's.- A single
'opens a quoted literal only when the next character is{,}, or — inside a plural or selectordinal branch, where#is syntax —#. Everything up to the closing'is literal text. - Anywhere else,
'is just an apostrophe.don't,it's, andl'éténeed no escaping and render unchanged. - An unterminated quote auto-closes at the end of the pattern rather than
throwing:
Use '{namerendersUse {name.
That makes '{' the way to emit a literal brace, and '{'/'}' the way to
show placeholder syntax to the reader:
| Pattern | Renders |
|---|---|
Ada''s file | Ada's file |
don't panic | don't panic |
'{'name'}' | {name} |
'{name}' | {name} |
Sale: 50'%' | Sale: 50'%' |
{n, plural, other {'#' rank}} | # rank |
Use '{name | Use {name |
Quoted runs become MessageLiteralNode entries in getMessageNodes() output.
Application authors do not need to think about any of this. Runtime descriptors
escape authored apostrophes, while extracted catalog identities keep natural
prose such as Ada's file unchanged. The extractor only doubles an apostrophe
when it directly precedes generated ICU syntax: t`L'${title}` therefore
produces the catalog pattern L''{title} so the placeholder remains live.
Policy-aware compiled keys keep the catalog and runtime spellings aligned.
The rules above matter for translators hand-editing .po files, for catalogs
imported from a TMS, and for patterns passed directly to
formatMessagePattern() — all of which use standard ICU quoting.
One deliberate exception: a descriptor whose message is a string literal
— t({ message: "Hello {name}" }) — is the raw-ICU authoring surface.
Placeholders are written literally, the quoting rules above apply verbatim
(It''s {name}, L'{title} quotes the brace), and nothing is auto-escaped.
A descriptor whose message is a template literal is authored text like a
tagged template and is auto-escaped. The JSX message attribute —
<Trans message="Hello {name}" /> — is the same raw-ICU surface and is left
unescaped as well, unlike <Trans> children, which are authored text.
Two helpers back the host-adapter renderers and are public for custom
adapters: resolveChoice(node, value, locale?) selects the branch of a parsed
plural/select/selectordinal node (returning the branch nodes plus the operand
for #), and stringifyValue(value) is the string renderer's value
stringification, where Date values become deterministic ISO strings.