import { setBase, createCatalogFormatter, type SemanticMessage } from '@datamoc/mw_games/i18n';
setBase({
locale: 'en',
direction: 'ltr',
messages: {
'combat.damage.log': 'The {target} takes {amount} damage.',
'combat.damage.compact': '-{amount} HP',
},
});
const formatter = createCatalogFormatter();
const message: SemanticMessage = { type: 'combat.damage', params: { target: 'gnoll', amount: 7 } };
console.log(formatter.format(message, 'log')); // 'The gnoll takes 7 damage.'
console.log(formatter.format(message, 'compact')); // '-7 HP'
console.log(formatter.format(message, 'debug')); // falls back to the bare type: the key itself, if undeclared
console.log(formatter.format(message, 'audio')); // 'sounds/sword-hit.wav', or '' when no '<type>.audio' entry exists
The audio channel is deliberately narrower than the text channels: it resolves only
<type>.audio, never falling back to the bare type the way text does. A bare type
holding a sentence ('combat.damage': 'combat.damage generic') is not a playable path,
so falling back to it would hand the game garbage to load. Sounds that need no
per-channel variation are still shared - every text channel already falls back to the
same bare type, while audio stays a separate entry a translator never touches and the
base language usually owns alone (runtime lookup falls back to the base catalog, so a
translated catalog needs no copy of it).
The reference
MessageFormatter: looks up${type}.${channel}in the active catalog (setBase/setActive,src/i18n/index.ts), falling back to${type}alone for a message that reads the same on every channel, then formats throught()- the same interpolation, pluralisation and RTL handling every other message in the game already gets. A catalog missing both keys fails the same observable wayt()already does for any key: returning the lookup key itself, rather than throwing or going silent.