SkillAgentSearch skills...

Mfml

#️⃣ The ICU MessageFormat + XML/HTML compiler and runtime that makes your translations tree-shakeable.

Install / Use

/learn @smikhalevski/Mfml
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<br/> <p align="center"> <a href="#readme"><picture> <source media="(prefers-color-scheme: dark)" srcset="./images/logo-dark.png" /> <source media="(prefers-color-scheme: light)" srcset="./images/logo-light.png" /> <img alt="MFML" src="./images/logo-light.png" width="400" /> </picture></a> </p> <br/> <!--ARTICLE--> <!--OVERVIEW-->

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.
<!--/OVERVIEW--> <br>
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

<span class="toc-icon">🪵 </span>Tokenizing messages

<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

View on GitHub
GitHub Stars4
CategoryDevelopment
Updated5mo ago
Forks0

Languages

TypeScript

Security Score

87/100

Audited on Oct 29, 2025

No findings