Rh.MessageFormat
0.8.2
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
<PackageReference Include="Rh.MessageFormat" Version="0.8.2" />
<PackageVersion Include="Rh.MessageFormat" Version="0.8.2" />
<PackageReference Include="Rh.MessageFormat" />
paket add Rh.MessageFormat --version 0.8.2
#r "nuget: Rh.MessageFormat, 0.8.2"
#:package Rh.MessageFormat@0.8.2
#addin nuget:?package=Rh.MessageFormat&version=0.8.2
#tool nuget:?package=Rh.MessageFormat&version=0.8.2
Rh.MessageFormat for .NET
A high-performance .NET implementation of the ICU Message Format standard with full CLDR support.
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:
- Exact match - e.g.,
en-GB - Base locale - e.g.,
en-GB→en - 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:
- Downloads CLDR JSON data from the official repository
- Parses plural/ordinal rules, currency data, units, date patterns, list patterns
- 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.
Links
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.ObjectPool (>= 8.0.10)
- Rh.MessageFormat.Abstractions (>= 0.8.2)
- Rh.MessageFormat.CldrData (>= 0.8.2)
-
net8.0
- Microsoft.Extensions.ObjectPool (>= 8.0.10)
- Rh.MessageFormat.Abstractions (>= 0.8.2)
- Rh.MessageFormat.CldrData (>= 0.8.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.