⧟ MessageMap
Dynamic string generation with a strongly-typed interface.
💁🏼♂️ Introduction
MessageMap
is an extra spicy version of String.prototype.replace
that applies TypeScript static typing to the concept of C-style string interpolation. Some potential use-cases include:
- Centrally manage error messages for your JavaScript/TypeScript package.
- Build a library of reactive translations for your front-end web application.
- Protect yourself from formatting bugs inside large/unwieldy template strings by maintaining type safety.
- Probably a lot more I haven't considered yet!
NOTE: This package uses modern TypeScript features to achieve type safety,
typescript@>=3.1
is recommended!
🔗 Installation
Install via yarn
(recommended):
yarn add message-map
Install via npm
:
npm install message-map
🛠️ Usage
Basic example
MessageMap
has a chaining API where each method returns a new instance of MessageMap
, similar to RxJS
. This ensures the type signatures remain accurate and improves readability!
import { MessageMap } from 'message-map';
const myStringBuilder = new MessageMap('The date is %month %day, %year. The current epoch is %epoch.')
.required('month')
.required('day')
.required('year')
.optional('epoch', () => String(new Date().getTime())); // We can choose to specify a default value for the optional key.
myStringBuilder.toString({
month: 'January',
day: '1',
year: '2000',
epoch: ... // Optional -- as indicated above. TypeScript will not complain if this prop is missing.
});
// => "The date is January 1, 2000. The current epoch is 946684800."
Using validator functions
You can optionally provide Validator
functions to MessageMap.required
or MessageMap.optional
. The callback returns a boolean
to indicate whether the interpolation should proceed at all OR a string
fallback in case the user-provided substitution is missing or undefined
(this enables you to specify defaults for MessageMap.optional
replacement keys—as shown in the above example for epoch
).
import { MessageMap } from 'message-map';
const myStringBuilder = new MessageMap('My phone number is %phoneNumber')
.required('phoneNumber', str => /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/.test(str));
myStringBuilder.toString({
phoneNumber: '555-asdf-1234' // This will raise an error because the phone number won't pass validation!
});
MessageCollection
Using The MessageCollection
class is intended for large-scale use-cases (such as building a language library) and is configurable via JSON-compatible objects. With TypeScript's resolveJsonModule
option enabled, or by providing a static object literal, your MessageCollection
can be strongly-typed, too!
import * as languagelibrary from '../path/to/languageLibrary.json'; // Requires `resolveJsonModule` in tsconfig.json
import { MessageCollection } from 'message-map';
const myLanguageLibrary = new MessageCollection(languageLibrary);
languageLibrary.json
has the following type signature:
interface JsonLanguageLibrary {
[key: string]: string | {
template: string;
optional?: {
[substitution: string]: string | null | {
default?: string;
regex?: string;
};
};
required?: {
[substitution: string]: string | null | {
default?: string;
regex?: string;
};
};
};
}
Here's some example JSON:
{
"HELLO_X": {
"template": "Good %partOfDay, %yourName!",
"optional": {
"yourName": {
"default": "Jeeves"
}
},
"required": {
"partOfDay": {
"regex": "//(morning|afternoon|evening)//"
}
}
},
"MORNING": "morning"
}
You can then use myLanguageLibrary
as a collection of static MessageMap
instances:
myLanguageLibrary.get('HELLO_X').toString({
yourName: 'Bojack',
partOfDay: myLanguageLibrary.get('MORNING').toString(),
});
// => "Good morning, Bojack!"
For convenience, the MessageCollection
class includes an enum of valid key names:
myLanguageLibrary.keys.HELLO_X; // => "HELLO_X"
myLanguageLibrary.keys.MORNING; // => "MORNING"
keyof typeof myLanguageLibrary.keys; // => "HELLO_X" | "MORNING"
The underlying MessageMap
instances are exposed on MessageCollection.messages
, but note they are lazily populated by MessageCollection.get(...)
. The MessageCollection.messages
property is exposed primarily for TypeScript typing needs:
myLanguageLibrary.messageMaps
// => {
// "HELLO_X"?: MessageMap,
// "MORNING"?: MessageMap,
// }
Then you can trivially extract the substitution parameters like so:
type MySubstitutionKeys = keyof typeof myLanguageLibrary['keys'];
type MySubstitutionConfig<TKey extends MySubstitutionKeys> = Parameters<typeof myLanguageLibrary['messages'][TKey]['toString']>;
Using the UMD build
A UMD build of MessageMap
is available via UNPKG. Drop it into your page via a <script>
tag:
<script type="text/javascript" src="https://unpkg.com/message-map/dist/index.umd.js"></script>
The API for the UMD build has one small difference: MessageMap
is the default object with MessageCollection
available on MessageMap.Collection
. So, you would use...
new MessageMap.Collection(langaugeLibraryJson);
Voila!