Skip to content

Commit e03f9bd

Browse files
Add docs for Angular MF2 integration (#58)
Co-authored-by: Luca Casonato <[email protected]>
1 parent 62ec8ba commit e03f9bd

File tree

3 files changed

+323
-5
lines changed

3 files changed

+323
-5
lines changed

docs/_data.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ sidebar:
1818
- docs/integration/java/
1919
- docs/integration/c/
2020
- docs/integration/cpp/
21+
- docs/frameworks/angular/
2122
- docs/lsp/

docs/frameworks/angular.md

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
---
2+
title: Using MF2 with Angular
3+
sidebar_title: Angular
4+
---
5+
6+
This guide explains how to localize Angular applications with MessageFormat 2
7+
(MF2), using the `angular-mf2` package.
8+
9+
The library:
10+
11+
- takes care of locale selection, MF2 parsing, and safe HTML rendering
12+
- exposes a tiny store service for locale and formatting
13+
- integrates with Angular via dependency injection (DI) tokens and an impure
14+
`| mf2` pipe
15+
16+
You bring the message catalogs and the formatting parameters; `angular-mf2` does
17+
the rest.
18+
19+
## Installation and setup
20+
21+
In an existing Angular project, install the Angular integration package together
22+
with the MF2 engine and an HTML sanitizer:
23+
24+
```sh
25+
npm install angular-mf2 messageformat sanitize-html
26+
```
27+
28+
You can also use a different package manager, such as `yarn`, `pnpm`, or `deno`
29+
to install the packages.
30+
31+
### Defining your catalogs
32+
33+
`angular-mf2` expects a catalog object mapping locale codes to message maps:
34+
35+
```ts
36+
// app.config.ts
37+
import { ApplicationConfig } from "@angular/core";
38+
import { useMF2Config } from "angular-mf2";
39+
40+
const catalogs = {
41+
en: {
42+
greeting: "Hello {$name}, how are you?",
43+
rich:
44+
"This is {#bold}bold{/bold}, {#italic}italic{/italic}, {#underline}underlined{/underline}, inline {#code}code(){/code}.",
45+
block:
46+
"{#p}Paragraph one.{/p}{#p}Paragraph two with a {#mark}highlight{/mark}.{/p}",
47+
quote:
48+
"{#quote}“Simplicity is the soul of efficiency.” — Austin Freeman{/quote}",
49+
list:
50+
"{#p}List:{/p}{#ul}{#li}First{/li}{#li}Second{/li}{#li}Third{/li}{/ul}",
51+
ordlist:
52+
"{#p}Steps:{/p}{#ol}{#li}Plan{/li}{#li}Do{/li}{#li}Review{/li}{/ol}",
53+
supSub: "H{#sub}2{/sub}O and 2{#sup}10{/sup}=1024",
54+
codeBlock: "{#pre}npm i angular-mf2{/pre}",
55+
},
56+
no: {
57+
greeting: "Hei {$name}, hvordan går det?",
58+
rich:
59+
"Dette er {#bold}fet{/bold}, {#italic}kursiv{/italic}, {#underline}understreket{/underline}, inline {#code}kode(){/code}.",
60+
block:
61+
"{#p}Avsnitt én.{/p}{#p}Avsnitt to med en {#mark}utheving{/mark}.{/p}",
62+
quote:
63+
"{#quote}«Enkelhet er effektivitetens sjel.» — Austin Freeman{/quote}",
64+
list:
65+
"{#p}Liste:{/p}{#ul}{#li}Første{/li}{#li}Andre{/li}{#li}Tredje{/li}{/ul}",
66+
ordlist:
67+
"{#p}Steg:{/p}{#ol}{#li}Plan{/li}{#li}Gjør{/li}{#li}Evaluer{/li}{/ol}",
68+
supSub: "H{#sub}2{/sub}O og 2{#sup}10{/sup}=1024",
69+
codeBlock: "{#pre}npm i angular-mf2{/pre}",
70+
},
71+
} as const;
72+
```
73+
74+
Instead of hardcoding the catalogs, you can also load them from JSON files:
75+
76+
```ts
77+
import en from "./locales/en.json" with { type: "json" };
78+
import no from "./locales/no.json" with { type: "json" };
79+
const catalogs = { en, no } as const;
80+
```
81+
82+
The shape of the catalogs object is as follows:
83+
84+
```ts
85+
type MF2Catalogs = Record<string, Record<string, string>>;
86+
```
87+
88+
Every key in the inner locale-specific maps corresponds to a message in MF2
89+
format. The keys are the ids you will use in your templates to reference the
90+
messages.
91+
92+
### Providing configuration via DI
93+
94+
The catalog and default locale must be provided via Angular's dependency
95+
injection (DI) system at application bootstrap time. This is done with the
96+
`useMF2Config(...)` helper function.
97+
98+
```ts
99+
// app.config.ts
100+
import { ApplicationConfig, provideHttpClient } from "@angular/core";
101+
import { useMF2Config } from "angular-mf2";
102+
103+
export const appConfig: ApplicationConfig = {
104+
providers: [
105+
provideHttpClient(),
106+
...useMF2Config({
107+
defaultLocale: "en",
108+
catalogs,
109+
}),
110+
],
111+
};
112+
```
113+
114+
This does two things:
115+
116+
- It registers the MF2 configuration object under the `MF2_CONFIG` DI token, so
117+
that the library can access it.
118+
- It registers the `Store` service, which is used by the `| mf2` pipe and can
119+
also be injected into your own components and services.
120+
121+
The shape of the configuration object is as follows:
122+
123+
```ts
124+
export type MF2Config = {
125+
defaultLocale: string;
126+
catalogs: Record<string, Record<string, string>>;
127+
};
128+
```
129+
130+
## Using the `| mf2` pipe in templates
131+
132+
The `MF2Pipe` is a standalone, impure pipe that returns sanitized HTML. Because
133+
of this, it is typically used together with `[innerHTML]`.
134+
135+
The basic usage is as follows:
136+
137+
```html
138+
<p [innerHTML]="'greeting' | mf2 : { name: username }"></p>
139+
<p [innerHTML]="'rich' | mf2"></p>
140+
<div [innerHTML]="'list' | mf2"></div>
141+
```
142+
143+
The signature of the pipe is `{{ key | mf2 : args? }}`, where `key` is a string
144+
key into the current locale's catalog, and `args` is an optional object mapping
145+
MF2 variable names to values. The pipe returns a sanitized HTML string that can
146+
be bound to `[innerHTML]`.
147+
148+
### Standalone import
149+
150+
The pipe is standalone and can be imported directly into components:
151+
152+
```ts
153+
import { Component } from "@angular/core";
154+
import { MF2Pipe } from "angular-mf2";
155+
156+
@Component({
157+
selector: "app-example",
158+
standalone: true,
159+
imports: [MF2Pipe],
160+
template: `
161+
<p [innerHTML]="'greeting' | mf2 : { name: 'Ada' }"></p>
162+
`,
163+
})
164+
export class ExampleComponent {}
165+
```
166+
167+
Because the pipe is **impure**, Angular will re-evaluate it whenever the `Store`
168+
emits changes (e.g. when the locale is switched).
169+
170+
## Store service
171+
172+
Internally, the pipe uses a small `Store` service that is also available for
173+
direct use in your own components and services.
174+
175+
```ts
176+
import { Injectable } from "@angular/core";
177+
import { Store } from "angular-mf2";
178+
179+
@Injectable()
180+
export class LocaleSwitcher {
181+
constructor(private readonly store: Store) {}
182+
183+
set(lang: string) {
184+
this.store.setLocale(lang);
185+
}
186+
}
187+
```
188+
189+
The API is intentionally small:
190+
191+
```ts
192+
@Injectable()
193+
class Store {
194+
setLocale(locale: string): void;
195+
getLocale(): string;
196+
format(key: string, args?: Record<string, unknown>): string;
197+
}
198+
```
199+
200+
- `setLocale(locale)` updates the active locale and notifies the pipe
201+
- `getLocale()` returns the currently active locale
202+
- `format(key, args?)` formats a given key programmatically and returns a
203+
**sanitized HTML string**
204+
205+
Example (programmatic formatting):
206+
207+
```ts
208+
@Component({
209+
/* ... */
210+
})
211+
export class GreetingComponent {
212+
html: string = "";
213+
214+
constructor(private readonly store: Store) {}
215+
216+
ngOnInit() {
217+
this.html = this.store.format("greeting", { name: "Lin" });
218+
}
219+
}
220+
```
221+
222+
```html
223+
<p [innerHTML]="html"></p>
224+
```
225+
226+
## Security and sanitization
227+
228+
`angular-mf2` uses `sanitize-html` with a conservative default policy. All
229+
formatted output is passed through the sanitizer before it is returned by the
230+
`Store` or `| mf2` pipe.
231+
232+
The default allowlist includes a subset of HTML elements such as:
233+
234+
- inline: `strong`, `em`, `u`, `s`, `code`, `kbd`, `mark`, `sup`, `sub`, `span`,
235+
`a`, `br`
236+
- block: `p`, `ul`, `ol`, `li`, `pre`, `blockquote`
237+
238+
Only a minimal set of attributes is allowed by default.
239+
240+
### Customizing the policy
241+
242+
You can extend or tighten the sanitizer configuration using the
243+
`MF2_SANITIZE_OPTIONS` DI token:
244+
245+
```ts
246+
// app.config.ts
247+
import { MF2_SANITIZE_OPTIONS } from "angular-mf2";
248+
249+
export const appConfig: ApplicationConfig = {
250+
providers: [
251+
...useMF2Config({ defaultLocale: "en", catalogs }),
252+
{
253+
provide: MF2_SANITIZE_OPTIONS,
254+
useValue: {
255+
allowedAttributes: {
256+
a: ["href", "target", "rel", "title", "role", "tabindex"],
257+
},
258+
allowedStyles: {
259+
"*": {
260+
color: [/^.*$/],
261+
"background-color": [/^.*$/],
262+
},
263+
},
264+
},
265+
},
266+
],
267+
};
268+
```
269+
270+
This object is passed directly to `sanitize-html`, so any options supported by
271+
that library can be used here.
272+
273+
## Markup cheatsheet
274+
275+
`angular-mf2` understands a small, explicit subset of MF2 markup and maps it to
276+
HTML elements. All output is sanitized; only the tags listed (and a minimal set
277+
of attributes) are allowed by default.
278+
279+
| MF2 Markup | Renders as |
280+
| --------------------------- | ---------------------------- |
281+
| `{#bold}x{/bold}` | `<strong>x</strong>` |
282+
| `{#italic}x{/italic}` | `<em>x</em>` |
283+
| `{#underline}x{/underline}` | `<u>x</u>` |
284+
| `{#s}x{/s}` | `<s>x</s>` |
285+
| `{#code}x{/code}` | `<code>x</code>` |
286+
| `{#kbd}⌘K{/kbd}` | `<kbd>⌘K</kbd>` |
287+
| `{#mark}x{/mark}` | `<mark>x</mark>` |
288+
| `{#sup}x{/sup}` | `<sup>x</sup>` |
289+
| `{#sub}x{/sub}` | `<sub>x</sub>` |
290+
| `{#p}x{/p}` | `<p>x</p>` |
291+
| `{#quote}x{/quote}` | `<blockquote>x</blockquote>` |
292+
| `{#ul}{#li}x{/li}{/ul}` | `<ul><li>x</li></ul>` |
293+
| `{#ol}{#li}x{/li}{/ol}` | `<ol><li>x</li></ol>` |
294+
| `{#pre}x{/pre}` | `<pre>x</pre>` |
295+
296+
Examples from the catalog above:
297+
298+
```mf2
299+
rich = This is {#bold}bold{/bold}, {#italic}italic{/italic}, {#underline}underlined{/underline}, inline {#code}code(){/code}.
300+
block = {#p}Paragraph one.{/p}{#p}Paragraph two with a {#mark}highlight{/mark}.{/p}
301+
list = {#p}List:{/p}{#ul}{#li}First{/li}{#li}Second{/li}{#li}Third{/li}{/ul}
302+
codeBlock = {#pre}npm i angular-mf2{/pre}
303+
```
304+
305+
Rendered via:
306+
307+
```html
308+
<p [innerHTML]="'rich' | mf2"></p>
309+
<div [innerHTML]="'block' | mf2"></div>
310+
<div [innerHTML]="'list' | mf2"></div>
311+
<pre [innerHTML]="'codeBlock' | mf2"></pre>
312+
```

docs/integration/js.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,31 @@ title: Using MF2 with JavaScript and TypeScript
33
sidebar_title: JavaScript/TypeScript
44
---
55

6-
The following guide explains how to use the `messageformat` package to format
7-
MF2 messages. For full API documentation, see the
6+
The following guide explains how to use the `messageformat` NPM package to
7+
format MF2 messages. For full API documentation, see the
88
[API documentation site](https://messageformat.github.io/modules/messageformat)
99
for the package. This guide shows the simplest use cases for the API. More
1010
advanced uses are possible, which are documented in the API documentation.
1111

12+
> Are you looking for information on using MF2 with a specific web framework? A
13+
> number of higher-level framework-specific integration packages are available:
14+
>
15+
> - [Angular](../../frameworks/angular)
16+
1217
## Installation and setup
1318

1419
Install/add the package to your project. If you're using Node.js with npm, you
1520
can do this by running
1621

1722
```sh
18-
npm install --save-exact messageformat@next
23+
npm install --save-exact messageformat
1924
```
2025

2126
or use the corresponding operation for your preferred package manager. For
2227
instance, for Deno projects, you can run
2328

2429
```sh
25-
deno add npm:messageformat@next
30+
deno add npm:messageformat
2631
```
2732

2833
once that's done, you can import the contructor in your code like
@@ -182,7 +187,7 @@ providing definitions for custom functions. To start with, let's write code for
182187
a simple custom function:
183188

184189
```js
185-
import { DefaultFunctions, asString } from "messageformat@next/functions";
190+
import { asString, DefaultFunctions } from "messageformat@next/functions";
186191

187192
/** @type {typeof DefaultFunctions.string} */
188193
function uppercase(context, options, input) {

0 commit comments

Comments
 (0)