Mfml
#️⃣ The ICU MessageFormat + XML/HTML compiler and runtime that makes your translations tree-shakeable.
Install / Use
/learn @smikhalevski/MfmlREADME
The ICU MessageFormat + XML/HTML compiler and runtime that makes your i18n messages tree-shakeable.
- TypeScript-first.
- Tree-shakeable: colocates messages with the code that uses them.
- Integrates with any translation management system.
- Highly customizable.
- First-class React support.
- Zero dependencies.
- XSS-resilient: no dangerous HTML rendering.
- Just 2 kB gzipped.
npm install --save-prod mfml
<br>
<!--/ARTICLE-->
<!--TOC-->
<span class="toc-icon">🔰 </span>Quick start
<span class="toc-icon">⚛️ </span>Rendering
<span class="toc-icon">⚙️ </span>Devtool
<span class="toc-icon">🛠️ </span>Configuration
messagesoutDirpackageNamefallbackLocalespreprocessorspostprocessorsrenameMessageFunctiondecodeTextgetArgumentTSTypetokenizerOptions
<span class="toc-icon">🪵 </span>Tokenizing messages
voidTagsrawTextTagsimplicitlyClosedTagsimplicitlyOpenedTagsisCaseInsensitiveTagsisSelfClosingTagsRecognizedisUnbalancedStartTagsImplicitlyClosedisUnbalancedEndTagsIgnoredisRawTextInterpolatedisOctothorpeRecognized
<span class="toc-icon">🌲 </span>Parsing messages
<span class="toc-icon">🎯 </span>Motivation
<!--/TOC--> <!--ARTICLE-->Quick start
Put your i18n messages in messages.json, grouped by locale:
{
"en": {
"greeting": "Hello, <b>{name}</b>!"
},
"ru": {
"greeting": "Привет, <b>{name}</b>!"
}
}
Put your config in mfml.config.js:
import { defineConfig } from 'mfml/compiler';
import messages from './messages.json' with { type: 'json' };
export default defineConfig({
messages,
});
Compile messages:
npx mfml
This would create the @mfml/messages npm package in node_modules directory. You can configure
the package name and the output directory to your liking.
In your application code, import message functions to produce formatted text:
import { renderToString } from 'mfml';
import { greeting } from '@mfml/messages';
renderToString({
message: greeting,
values: { name: 'Bob' },
locale: 'en',
});
// ⮕ 'Hello, Bob!'
Or render messages with React:
import { Message, MessageLocaleProvider } from 'mfml/react';
import { greeting } from '@mfml/messages';
export const App = () => (
<MessageLocaleProvider value={'en-US'}>
<Message
message={greeting}
values={{ name: 'Bob' }}
/>
</MessageLocaleProvider>
);
This renders markup with HTML elements:
Hello, <b>Bob</b>!
Now, your bundler would do all the heavy lifting and colocate message functions with components that import them in the same chunk.
Syntax overview
The MFML syntax is a hybrid of the ICU MessageFormat syntax and XML/HTML.
ICU MessageFormat is a templating syntax designed for internationalized messages. It allows developers to insert variables and handle pluralization, gender, and selection logic in a locale-aware way. MFML supports all ICU MessageFormat features and allows you to customize and extend them.
Here's the basic argument syntax:
{name}
Enable formatting by specifying data type and style:
{age, number, integer}
Select over argument categories:
{gender, select,
male { He is a # }
female { She is a # }
}
Pluralization is handled through argument categories as well:
You have {messageCount, plural,
one { one message }
other { # messages }
}
MFML supports XML/HTML tags and attributes:
Hello, <strong>{name}</strong>!
Arguments can be used where XML/HTML allows text:
<abbr title="Greetings to {name}">Hello, {name}!</abbr>
You can use your custom tags and setup a custom renderer to properly display them:
<Hint title="Final offer at {discount, percent} discount!">{price}</Hint>
Arguments
The most basic use case is argument placeholder replacement:
Hello, {name}!
Here, {name} is an argument that doesn't impose any formatting on its value. Spaces around the argument name are
ignored, so this yields the same result:
Hello, { name }!
By default, during interpolation, the argument values are cast to string.
Types and styles
Argument values can be formatted during interpolation. Provide an argument type to select the formatter to use:
You have {count, number} messages.
^^^^^^
Here, number is an argument type. By default, number type uses
Intl.NumberFormat
for formatting.
You can also provide a style for a formatter:
Download {progress, number, percent} complete.
^^^^^^^
Default configuration provides following argument types and styles:
<table> <tr> <th align="left">Argument type</th> <th align="left">Argument style</th> <th align="left">Example, <code>en</code> locale</th> <th align="left">Required value type</th> </tr> <tr> <td valign="top" rowspan="5"><code>number</code></td> <td>—</td> <td>1,000.99</td> <td valign="top" rowspan="5"><code>number</code> or <code>bigint</code></td> </tr> <tr> <td><code>decimal</code></td> <td>1,000.99</td> </tr> <tr> <td><code>integer</code></td> <td>1,000</td> </tr> <tr> <td><code>percent</code></td> <td>75%</td> </tr> <tr> <td><code>currency</code></td> <td>$1,000.00</td> </tr> <tr> <td valign="top" rowspan="5"><code>date</code></td> <td>—</td> <td>1/1/1970</td> <td valign="top" rowspan="5"><code>number</code> or <code>Date</code></td> </tr> <tr> <td><code>short</code></td> <td>1/1/70</td> </tr> <tr> <td><code>medium</code></td> <td>Jan 1, 1970</td> </tr> <tr> <td><code>long</code></td> <td>January 1, 1970</td> </tr> <tr> <td><code>full</code></td> <td>Thursday, January 1, 1970</td> </tr> <tr> <td valign="top" rowspan="5"><code>time</code></td> <td>—</td> <td>12:00 AM</td> <td valign="top" rowspan="5"><code>number</code> or <code>Date</code></td> </tr> <tr> <td><code>short</code></td> <td>12:00 AM</td> </tr> <tr> <td><code>medium</code></td> <td>12:00:00 AM</td> </tr> <tr> <td><code>long</code></td> <td>12:00:00 AM UTC</td> </tr> <tr> <td><code>full</code></td> <td>12:00:00 AM Coordinated Universal Time</td> </tr> <tr> <td valign="top" rowspan="4"><code>conjunction</code></td> <td>—</td> <td>A, B, and C</td> <td valign="top" rowspan="4"><code>string[]</code></td> </tr> <tr> <td><code>narrow</code></td> <td>A, B, C</td> </tr> <tr> <td><code>short</code></td> <td>A, B, & C</td> </tr> <tr> <td><code>long</code></td> <td>A, B, and C</td> </tr> <tr> <td valign="top" rowspan="4"><code>disjunction</code></td> <td>—</td> <td>A, B, or C</td> <td valign="top" rowspan="4"><code>string[]</code></td> </tr> <tr> <td><code>narrow</code></td> <td>A, B, or C</td> </tr> <tr> <td><code>short</code></td> <td>A, B, or C</td> </tr> <tr> <td><code>long</code></td> <td>A, B, or C</td> </tr> </table>Options
Instead of using a predefined style, you can provide a set of options for a formatter:
{propertyArea, number,
style=unit
unit=acre
unitDisplay=long
}
Here style, unit and unitDisplay are options of
the Intl.NumberFormat.
You can find the full list of options for
number arguments
and for
date and time arguments
on MDN.
Categories
Arguments can use categories for conditional rendering. For example, if you want to alter a message depending on a user's gender:
{gender, select,
male {He}
female {She}
other {They}
}
sent you a message.
Value of the gender argument is used for selecting a specific category. If the value is "male" then argument
placeholder is replaced with "He", if value is "female" then with "She", and for any other value "They" is rendered.
other is a special category: its value is used if no other category matches. If there's no matching category in
select and no `other
