|
| 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 | +``` |
0 commit comments