Saykit provides multiple ways to define messages using macros that are transformed at compile-time by the Babel plugin.
Basic Message Syntax
Tagged Template Literals
The most common way to define messages:
import say from './i18n' ;
const greeting = say `Hello, world!` ;
const personalized = say `Hello, ${ userName } !` ;
At compile-time, this becomes:
const greeting = say . call ({ id: "a1b2c3" });
const personalized = say . call ({ id: "d4e5f6" , 0 : userName });
How it works:
The Babel plugin detects the say tagged template
Extracts the message content: "Hello, {0}!"
Generates a stable hash ID based on the message
Transforms the template into a call() method
Location: packages/plugin-babel/src/features/js/parser.ts:13-42
With Placeholders
Placeholders are automatically numbered:
say `Your order of ${ quantity } ${ item } is ready!`
Extracted as:
Your order of {0} {1} is ready!
Placeholders are numbered in the order they appear in the template string.
Message Descriptors
Add custom IDs or context for disambiguation:
Custom ID
Provide a stable, semantic ID instead of a generated hash:
say ({ id: 'greeting.hello' }) `Hello!`
say ({ id: 'button.submit' }) `Submit`
Context Parameter
Disambiguate identical strings with different meanings:
// "Right" as in direction
say ({ context: 'direction' }) `Right`
// "Right" as in correctness
say ({ context: 'correctness' }) `Right`
These generate different IDs and extract as separate messages:
msgctxt "direction"
msgid "Right"
msgstr ""
msgctxt "correctness"
msgid "Right"
msgstr ""
Location: packages/integration/src/runtime.ts:43-56
Combined
You can use both id and context:
say ({ id: 'direction.right' , context: 'navigation' }) `Right`
Pluralization
Handle plural forms using CLDR rules:
say . plural ( itemCount , {
one: 'You have 1 item' ,
other: 'You have # items' ,
})
The # symbol is replaced with the numeric value in the message.
Extracted as:
{0, plural,
one {You have 1 item}
other {You have # items}
}
CLDR Plural Categories
Support all CLDR categories:
say . plural ( count , {
zero: 'No items' ,
one: '1 item' ,
two: '2 items' ,
few: 'A few items' ,
many: 'Many items' ,
other: '# items' ,
})
Different locales use different plural rules. For example, English only uses one and other, but Arabic uses all six categories.
Exact Numbers
Match exact numbers explicitly:
say . plural ( count , {
0 : 'No items' ,
1 : 'One item' ,
other: '# items' ,
})
Location: packages/integration/src/runtime.ts:320-329
Ordinals
Format ordinal numbers (1st, 2nd, 3rd):
say . ordinal ( position , {
1 : '#st' ,
2 : '#nd' ,
3 : '#rd' ,
other: '#th' ,
})
Or with CLDR categories:
say . ordinal ( position , {
one: '#st' ,
two: '#nd' ,
few: '#rd' ,
other: '#th' ,
})
Location: packages/integration/src/runtime.ts:350-359
Select
Branch based on a string value:
say . select ( gender , {
male: 'He is online' ,
female: 'She is online' ,
other: 'They are online' ,
})
Extracted as:
{0, select,
male {He is online}
female {She is online}
other {They are online}
}
Useful for:
Gender selection
Status messages
Role-based text
Any categorical branching
Location: packages/integration/src/runtime.ts:378-387
JSX Syntax (React)
For React applications, use the <Say> component:
import { Say } from '@saykit/react' ;
< Say > Welcome to our app! </ Say >
< Say >
Hello, < strong > { userName } </ strong > !
</ Say >
Extracted as:
Welcome to our app!
Hello, <0>{userName}</0>!
JSX elements are numbered like placeholders.
Location: packages/plugin-babel/src/features/jsx/parser.ts
Add context for translators using special comments:
// translators: This appears on the login button
say `Sign in`
// translators: Username field label in the registration form
say `Username`
Comments starting with translators: are extracted to the message file:
# This appears on the login button
msgid "Sign in"
msgstr ""
# Username field label in the registration form
msgid "Username"
msgstr ""
Location: packages/plugin-babel/src/features/js/parser.ts:214-221
When you run saykit extract, messages are written to files in the format specified by your formatter (default: PO).
Default extraction format:
msgid ""
msgstr ""
" POT-Creation-Date : 2025-12-29 11:02+1300\n "
" Content-Type : text/plain; charset=UTF-8\n "
" Language : en\n "
" X-Generator : saykit\n "
#: src/commands/about.ts:16
msgid "about"
msgstr "about"
#: src/commands/about.ts:40
msgid "Your name is {name}!"
msgstr "Your name is {name}!"
#: src/commands/ping.ts:19
msgid "Pong! Latency is {latency}ms."
msgstr "Pong! Latency is {latency}ms."
Key elements:
#: - Source file references
msgid - The source message
msgstr - The translation (empty for source locale)
Example: examples/carbon-tsdown/src/locales/en/messages.po
After running saykit compile, messages are converted to JSON for runtime:
{
"a1b2c3d4" : "Hello, {0}!" ,
"e5f6g7h8" : "Your name is {name}!" ,
"i9j0k1l2" : "Pong! Latency is {latency}ms."
}
This format is optimized for fast runtime lookups.
Message ID Generation
Message IDs are generated using a stable hash algorithm:
function generateHash ( message : string , context ?: string ) : string {
// Generates a stable hash like "a1b2c3d4e5f6g7h8"
}
Properties:
Deterministic: Same message always generates the same ID
Context-aware: Context parameter affects the hash
Collision-resistant: Different messages produce different IDs
Location: packages/plugin-babel/src/core/messages/hash.ts
Custom IDs (via say({ id: '...' })) bypass hash generation and use your provided ID directly.
Best Practices
Use Semantic IDs for Important Messages For navigation, branding, or critical UI elements, use custom IDs: say ({ id: 'nav.home' }) `Home`
say ({ id: 'brand.tagline' }) `Your trusted partner`
Add Context for Ambiguous Strings When the same word has different meanings: say ({ context: 'noun' }) `Post`
say ({ context: 'verb' }) `Post`
Use Translator Comments Help translators understand context: // translators: Button text - keep it short (max 20 chars)
say `Continue`
Prefer Template Literals Over Concatenation ❌ Don’t: say\Hello` + `, ` + say`$“ ✅ Do: say\Hello, $“
Macros must be used with the Babel plugin configured. They will throw runtime errors if the plugin is not active.