npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

intl-messageformat-html

v1.0.11

Published

HTML tag functions for use with intl-messageformat

Downloads

259

Readme

intl-messageformat-html

HTML tag functions for use with intl-messageformat (docs). Almost all html elements and attributes are supported, although only a minimal set are recommended. Custom tag functions can also be easily integrated to use CSS to style your localized text.

Hi {name}! Welcome to <strong>intl-messageformat-html</strong>.

Installation

npm i intl-messageformat-html

Simple example

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = `
  Welcome to <strong>intl-messageformat-html</strong>!
  <em>Add HTML to your localized messages</em>.
  `;
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Resulting string:

  Welcome to <strong>intl-messageformat-html</strong>!
  <em>Add HTML to your localized messages</em>.

And when rendered as HTML:

Welcome to intl-messageformat-html!

Add HTML to your localized messages.

What did the library do?

In this example, it looks like intl-messageformat-html didn't really do anything, and that's in a way true. It reconstructed the same html that existed in the source messages. intl-messageformat-html is still necessary in this example though since intl-messageformat requires a tag function be defined for every tag used. By providing "passthrough" tag functions that recreate the same HTML then all simple HTML (without attributes) can be used in localization messages.

Attributes

While we recommend avoiding complex HTML inside localized messages, there are instances where some HTML with attributes are useful and appropriate. intl-messageformat-html supports almost all atributes for supported HTML elements.

intl-messageformat does not allow specifying attributes on tags so intl-messageformat-html supports specifying attributes as embedded tags.

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = 'Welcome to the German version (<image><src>/images/flag-de.png</src></image>).';
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Resulting string:

  Welcome to the German version (<image src="/images/flag-de.png" />).

And when rendered as HTML:

Welcome to the German version (German Flag).

Interpolated Values

The examples so far all passed the constant tagFunctions provided by intl-messageformat-html as the values argument to add support for HTML tags. You'll likely have your own values though to interpolate data into your localized strings. To support this, intl-messageformat-html provides a helper function wrapValues. This allows you to provide your own values for interpolation and also support HTML tags at the same time.

import { IntlMessageFormat } from 'intl-messageformat';
import { wrapValues } from 'intl-messageformat-html';

const values = {
  name: 'John',
};

const message = 'Hi {name}! Welcome to <strong>intl-messageformat-html</strong>.';
const html = new IntlMessageFormat(message, 'en').format(wrapValues(values));

Resulting string:

  Hi John! Welcome to <strong>intl-messageformat-html</strong>.

And when rendered as HTML:

Hi John! Welcome to intl-messageformat-html!

Safety and entity encoding

Generating dynamic HTML is dangerous since data from a user could inject malicious HTML into the document. intl-messageformat-html provides partial protection against this. Any content that comes from interpolated values is entity encoded. The messages themselves are not. The reason for this is the messages will contain tags, which must not be entity encoded, and nested tags, which are necessary for certain constructs, like lists and formatting inside links, will not render as desired if entity encoded.

With this in mind, and it's almost universal anyways, the messages themselves should not be user-generated content, or if they are, they should be validated to be safe before sending to intl-messageformat-html. This also means that messages that have content that must be entity-encoded are encoded in the source messages themselves.

import { IntlMessageFormat } from 'intl-messageformat';
import { wrapValues } from 'intl-messageformat-html';

const values = {
  name: 'John & Joe',
};

const message = 'Hi {name}! Welcome to <strong>Bob &amp; Bill&apos;s Service</strong>.';
const html = new IntlMessageFormat(message, 'en').format(wrapValues(values));

Resulting string:

  Hi John &amp; Joe! Welcome to <strong>Bob &amp; Bill&apos;s Service</strong>.

Note how the first & inside the interpolated values is not encoded ihe source but is encoded in the output and the & and ' in the message are entity-encoded before the message is processed. The result is that both are properly encoded in the output.

Links

Links are the only element given special treatment. By default any link will have the target attribute added automatically with a value of _blank. This attribute is not added if a target is explicitly provided in the message.

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = 'Check out the latest <a><href>https://cnn.com</href>news on CNN</a>.';
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Resulting string:

  Check out the latest <a href="https://cnn.com" target="_blank">news on CNN</a>.

And when rendered as HTML:

Check out the latest news on CNN.

And to override the default behavior:

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = '<a><href>#content</href><target>_self</target>Skip to content</a>';
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Resulting string:

  <a href="#content" target="_self">Skip to content</a>

And when rendered as HTML:

Skip to content

CSS Classes

In addition to standard HTML elements, intl-messageformat-html makes it easy to create content with custom CSS classes. To do this pass an array of class names to either createClassTagFunctions or as a second parameter to wrapValues. Each class name can be specified as a tag within the message.

Custom class tags are rendered as span elements.

import { IntlMessageFormat } from 'intl-messageformat';
import { createClassTagFunctions } from 'intl-messageformat-html';

const classes = [ 'value' ];
const message = 'Age must be specified as an <value>integer</value>.';
const html = new IntlMessageFormat(message, 'en').format(createClassTagFunctions(classes));

Resulting string:

  Age must be specified as an <span class="value">integer</span>.

And when rendered as HTML (given styling .value { text-transform: uppercase; }):

Age must be specified as an INTEGER.

SVG Elements

While it should probably be used very sparingly, SVG is also supported.

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = `
  Click on the red (<svg><width>16</width><height>16</height>
    <rect><width>16</width><height>16</height><style>fill:red;</style></rect>
  </svg>) box.
`;
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Resulting string:

  Click on the red (<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
    <rect width="16" height="16" style="fill:red;" />
  </svg>) box.

Note that the namespace is added automatically to the <svg> element, if not otherwise specified.

Performance

Localized strings can often be generated a hundred or more times in a single frame. Performance is critical in processing these strings and intl-messageformat-html has important characteristics to not degrade performance of localization.

Constant tagFunctions

The tagFunctions constant itself is of course a constant. It's generated once at the start of the application and never needs to change, regardless of usage or locale. Whenever you do not need your own interpolated values or custom CSS classes, passing tagFunctions will yield the best results.

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';

const message = 'Welcome to <strong>intl-messageformat-html</strong>!';
const html = new IntlMessageFormat(message, 'en').format(tagFunctions);

Multiproxy from utikity

When you do need your own interpolated values it's critical to use the wrapValues helper and not spread across your values and tagFunctions. The spread operator is the most common and generally recommended way to combine objects, but in this specific use case it is not optimal. Since your message will use only a small fraction of values from the tag functions, using spread to create a combined object will perform a lot more work than cherry-picking only the used functions.

Performance of wrapValues is achieved through use of a Multiproxy from utikity. Instead of creating a new object with the properties of your interpolated values and the tag functions, the Multiproxy is a proxy that will first return any requested value from your interpolated values and, if not found, then will return the value from the tag functions.

The code behind wrapValues is essentially like this (but has other performance optimizations too):

import { IntlMessageFormat } from 'intl-messageformat';
import { tagFunctions } from 'intl-messageformat-html';
import { createMultiproxy } from 'utikity';

const values = {
  name: 'John',
};

// Don't do this, it's just a demonstration
const combinedTags = createMultiproxy(values, tagFunctions);

const message = 'Hi {name}! Welcome to <strong>intl-messageformat-html</strong>.';
const html = new IntlMessageFormat(message, 'en').format(combinedTags);

Caching in wrapValues and createClassTagFunctions

It's also extremely common to call wrapValues and createClassTagFunctions repeatedly with the same set of values and classes. This is particularly true in React where components are rendered with the same inputs to identify if the resulting content actually changed. Therefor both wrapValues and createClassTagFunctions will cache the resulting multiproxy and custom tag functions object based on the input values and class list.

The pseudo-code representing this caching is essentially this:

const cache = new Map();

function createClassTagFunctions(classes) {
  if (cache.has(classes)) {
    return cache.get(classes);
  }
  const customTagFunctions = realCreateClassTagFunctions(classes);
  cache.set(classes, customTagFunctions);
  return customTagFunctions;
}

function wrapValues(values) {
  if (cache.has(values)) {
    return cache.get(values);
  }
  const multiproxy = createMultiproxy(values, tagFunctions);
  cache.set(classes, multiproxy);
  return multiproxy;
}

The cache doesn't actually store all values forever though. It will store approximately up to the cache size (default 100) and then purge the cache if the cache size is reached. The cache size is approximate because purging happens on the next frame so within a single frame, the cache size can be exceeded. When purged, the cache is reduced to at most 60% of the cache size depending on how long items have been in the cache and how often they've been used.

The cache size can be overridden and caching can be disabled entirely by setting the cache size to zero.

import { setTagFunctionCacheSize } from 'intl-messageformat-html';

// to cache more
setTagFunctionCacheSize(1000);

// to not cache at all
setTagFunctionCacheSize(0);