Rh.MessageFormat 0.8.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package Rh.MessageFormat --version 0.8.2
                    
NuGet\Install-Package Rh.MessageFormat -Version 0.8.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Rh.MessageFormat" Version="0.8.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rh.MessageFormat" Version="0.8.2" />
                    
Directory.Packages.props
<PackageReference Include="Rh.MessageFormat" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rh.MessageFormat --version 0.8.2
                    
#r "nuget: Rh.MessageFormat, 0.8.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Rh.MessageFormat@0.8.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rh.MessageFormat&version=0.8.2
                    
Install as a Cake Addin
#tool nuget:?package=Rh.MessageFormat&version=0.8.2
                    
Install as a Cake Tool

Rh.MessageFormat for .NET

A high-performance .NET implementation of the ICU Message Format standard with full CLDR support.

CI/CD NuGet License: MIT

Features

  • ICU Message Format - Full support for pluralization, selection, and nested messages
  • CLDR Data - Pre-compiled locale data for 200+ locales (plurals, ordinals, currencies, units, dates, lists)
  • High Performance - Hand-written parser, no regex, compiled plural rules, pattern caching
  • Custom Formatters - Extend with your own formatting functions
  • Rich Text Tags - Support for HTML/Markdown-style tags in messages
  • AOT Compatible - Fully trimmable and Native AOT ready
  • Modern .NET - Targets .NET 8.0 and .NET 10.0

Installation

dotnet add package Rh.MessageFormat

Or via Package Manager:

Install-Package Rh.MessageFormat

Quick Start

using Rh.MessageFormat;

// Create a formatter for a specific locale
var formatter = new MessageFormatter("en");

// Format a message with pluralization
var result = formatter.FormatMessage(
    "You have {count, plural, one {# notification} other {# notifications}}",
    new Dictionary<string, object?> { { "count", 5 } }
);
// Result: "You have 5 notifications"

Message Syntax

Simple Replacement

formatter.FormatMessage("Hello, {name}!", new { name = "World" });
// Result: "Hello, World!"

Pluralization

var pattern = @"{count, plural,
    zero {No items}
    one {One item}
    =42 {The answer}
    other {# items}
}";

formatter.FormatMessage(pattern, new { count = 0 });  // "No items"
formatter.FormatMessage(pattern, new { count = 1 });  // "One item"
formatter.FormatMessage(pattern, new { count = 42 }); // "The answer"
formatter.FormatMessage(pattern, new { count = 5 });  // "5 items"

Plural categories supported: zero, one, two, few, many, other

Ordinals

var pattern = "{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}";

formatter.FormatMessage(pattern, new { position = 1 });  // "1st"
formatter.FormatMessage(pattern, new { position = 2 });  // "2nd"
formatter.FormatMessage(pattern, new { position = 3 });  // "3rd"
formatter.FormatMessage(pattern, new { position = 4 });  // "4th"

Selection

var pattern = "{gender, select, male {He} female {She} other {They}} liked your post";

formatter.FormatMessage(pattern, new { gender = "female" });
// Result: "She liked your post"

Number Formatting

var formatter = new MessageFormatter("en");

// Basic number
formatter.FormatMessage("{n, number}", new { n = 1234.56 });
// Result: "1,234.56"

// Currency (using ICU skeleton)
formatter.FormatMessage("{price, number, ::currency/USD}", new { price = 99.99 });
// Result: "$99.99"

// Percent
formatter.FormatMessage("{rate, number, percent}", new { rate = 0.15 });
// Result: "15%"

// Compact notation
formatter.FormatMessage("{n, number, ::compact-short}", new { n = 1500000 });
// Result: "1.5M"

Date and Time Formatting

var now = DateTime.Now;

// Date with styles: short, medium, long, full
formatter.FormatMessage("{d, date, short}", new { d = now });
formatter.FormatMessage("{d, date, long}", new { d = now });

// Time with styles: short, medium, long, full
formatter.FormatMessage("{t, time, short}", new { t = now });

// DateTime combined
formatter.FormatMessage("{dt, datetime, medium}", new { dt = now });

List Formatting

var items = new[] { "Apple", "Banana", "Cherry" };

// Conjunction (and)
formatter.FormatMessage("{items, list}", new { items });
// Result: "Apple, Banana, and Cherry"

// Disjunction (or)
formatter.FormatMessage("{items, list, disjunction}", new { items });
// Result: "Apple, Banana, or Cherry"

// Unit list
formatter.FormatMessage("{items, list, unit}", new { items });
// Result: "Apple, Banana, Cherry"

Nested Messages

Messages can be nested to any depth:

var pattern = @"{gender, select,
    male {{count, plural, one {He has # apple} other {He has # apples}}}
    female {{count, plural, one {She has # apple} other {She has # apples}}}
    other {{count, plural, one {They have # apple} other {They have # apples}}}
}";

formatter.FormatMessage(pattern, new { gender = "female", count = 3 });
// Result: "She has 3 apples"

Configuration

MessageFormatterOptions

var options = new MessageFormatterOptions
{
    DefaultFallbackLocale = "en",  // Fallback when locale data not found
    CldrDataProvider = new CldrDataProvider(),  // CLDR data source
    CultureInfoCache = new CultureInfoCache()   // CultureInfo caching
};

var formatter = new MessageFormatter("de-DE", options);

Custom Formatters

Add your own formatting functions:

var options = new MessageFormatterOptions();

// Add a custom "money" formatter
options.CustomFormatters["money"] = (value, style, locale, culture) =>
{
    if (value is decimal amount)
        return amount.ToString("C", culture);
    return value?.ToString() ?? "";
};

var formatter = new MessageFormatter("en-US", options);
var result = formatter.FormatMessage("Total: {amount, money}", new { amount = 99.99m });
// Result: "Total: $99.99"

Custom formatter signature:

delegate string CustomFormatterDelegate(
    object? value,      // The value to format
    string? style,      // Optional style argument
    string locale,      // Current locale code
    CultureInfo culture // Current culture
);

Tag Handlers (Rich Text)

Process HTML/Markdown-style tags in messages:

var options = new MessageFormatterOptions();

// Add handlers for rich text tags
options.TagHandlers["bold"] = content => $"<strong>{content}</strong>";
options.TagHandlers["link"] = content => $"<a href='#'>{content}</a>";

var formatter = new MessageFormatter("en", options);
var result = formatter.FormatMessage(
    "Click <bold>here</bold> to <link>learn more</link>",
    new Dictionary<string, object?>()
);
// Result: "Click <strong>here</strong> to <a href='#'>learn more</a>"

Cached Provider

For applications that format messages in multiple locales, use MessageFormatterCachedProvider:

// Create a provider with pre-initialized locales
var locales = new[] { "en", "de-DE", "fr-FR", "es", "ja" };
var provider = new MessageFormatterCachedProvider(locales, options);

// Pre-load all formatters (optional, improves first-call performance)
provider.Initialize();

// Get formatter for any locale (cached automatically)
var enFormatter = provider.GetFormatter("en");
var deFormatter = provider.GetFormatter("de-DE");

// Formatters are cached and reused
var sameFormatter = provider.GetFormatter("en"); // Same instance as enFormatter

On-demand caching (without pre-initialization):

var provider = new MessageFormatterCachedProvider(options);
var formatter = provider.GetFormatter("en"); // Created and cached on first call

Locale Fallback

The formatter resolves locale data using a fallback chain:

  1. Exact match - e.g., en-GB
  2. Base locale - e.g., en-GBen
  3. Default fallback - Configured via DefaultFallbackLocale (default: en)
// If "en-AU" data is not available, falls back to "en"
var formatter = new MessageFormatter("en-AU", options);

Escaping

Use single quotes to escape special characters:

// Escape braces
formatter.FormatMessage("Use '{' and '}' for variables", new { });
// Result: "Use { and } for variables"

// Escape the # symbol in plural blocks
formatter.FormatMessage("{n, plural, other {'#' is the number: #}}", new { n = 5 });
// Result: "# is the number: 5"

// Double single quote for literal quote
formatter.FormatMessage("It''s working!", new { });
// Result: "It's working!"

Building from Source

Prerequisites

  • .NET 8.0 SDK or later
  • Bash (for CLDR data generation on Linux/macOS) or PowerShell (Windows)

Clone and Build

git clone https://github.com/Rheopyrin/Rh.Messageformat.git
cd Rh.Messageformat
dotnet restore
dotnet build

Run Tests

dotnet test

Generate CLDR Locale Data

The library ships with pre-generated CLDR data. To regenerate or customize:

Linux/macOS:

chmod +x src/scripts/sync-cldr.sh
./src/scripts/sync-cldr.sh

Windows (PowerShell):

.\src\scripts\sync-cldr.ps1

Options:

./src/scripts/sync-cldr.sh \
    --version 48.1.0 \              # CLDR version (default: latest)
    --locales "en,de-DE,fr-FR" \    # Specific locales (default: all)
    --working-dir /tmp/cldr \       # Working directory for downloads
    --keep-files                    # Keep downloaded files after generation

The generator:

  1. Downloads CLDR JSON data from the official repository
  2. Parses plural/ordinal rules, currency data, units, date patterns, list patterns
  3. Generates optimized C# code with compiled rules (no runtime parsing)

Project Structure

src/
├── Rh.MessageFormat/              # Main library
├── Rh.MessageFormat.Abstractions/ # Interfaces and models
├── Rh.MessageFormat.CldrData/     # Generated CLDR locale data
├── Rh.MessageFormat.CldrGenerator/ # CLDR data generator tool
└── scripts/                       # Build scripts

tests/
├── Rh.MessageFormat.Tests/                        # Unit tests
├── Rh.MessageFormat.CldrGenerator.Tests/          # Generator unit tests
└── Rh.MessageFormat.CldrGenerator.Tests.Integration/ # Integration tests

NuGet Packages

Package Description
Rh.MessageFormat Main library with all formatting features
Rh.MessageFormat.Abstractions Interfaces for extensibility
Rh.MessageFormat.CldrData Pre-compiled CLDR locale data

Performance

  • Hand-written parser - No parser generators or regular expressions
  • Compiled plural rules - CLDR rules pre-compiled to C# if/else chains
  • Pattern caching - Parsed AST cached for repeated formatting
  • Lazy-loaded data - CLDR data loaded on-demand per locale
  • StringBuilder pooling - Reduced allocations during formatting

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.1 31 1/22/2026
0.9.0 32 1/22/2026
0.8.4 33 1/21/2026
0.8.3 35 1/21/2026
0.8.2 35 1/21/2026