Skip to content

Rheopyrin/Rh.MessageFormat

Repository files navigation

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, dates)
  • Modular Architecture - Optional packages for units, lists, relative time, and date ranges to reduce package size
  • Rich Formatting - Numbers, dates, durations, number ranges, lists, and relative time
  • Spellout - Number to words conversion (cardinal, ordinal, year) via optional RBNF package
  • Number Skeletons - ICU number skeleton support including compact notation, ordinals, and currency
  • 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

Plural Offset

The offset feature allows subtracting a value from the plural argument before determining the plural category. This is useful for "excluding" items from the count (e.g., "You and 3 others" instead of "4 people").

var pattern = @"{count, plural, offset:1
    =0 {Nobody is attending}
    =1 {Only {host} is attending}
    one {{host} and # other person are attending}
    other {{host} and # other people are attending}
}";

formatter.FormatMessage(pattern, new { count = 0, host = "Alice" });  // "Nobody is attending"
formatter.FormatMessage(pattern, new { count = 1, host = "Alice" });  // "Only Alice is attending"
formatter.FormatMessage(pattern, new { count = 2, host = "Alice" });  // "Alice and 1 other person are attending"
formatter.FormatMessage(pattern, new { count = 5, host = "Alice" });  // "Alice and 4 other people are attending"

Key behaviors:

  • The offset is placed after plural, and before the cases: {n, plural, offset:1 ...}
  • Exact match cases (=0, =1, etc.) match against the original value, not the offset-adjusted value
  • Category selection (one, other, etc.) uses the offset-adjusted value
  • The # placeholder displays the offset-adjusted value
  • Offset can be any number (integer, decimal, or negative)

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"

Number Skeletons

Number skeletons provide fine-grained control over number formatting using ICU skeleton syntax. Skeletons are prefixed with :: in the style argument.

formatter.FormatMessage("{n, number, ::skeleton-tokens}", new { n = value });

Precision

Fraction Digits (prefix with .):

formatter.FormatMessage("{n, number, ::.00}", new { n = 3.1 });    // "3.10" - exactly 2 fraction digits
formatter.FormatMessage("{n, number, ::.##}", new { n = 3.0 });    // "3" - at most 2 fraction digits
formatter.FormatMessage("{n, number, ::.0#}", new { n = 3.1 });    // "3.1" - 1 to 2 fraction digits
formatter.FormatMessage("{n, number, ::.00*}", new { n = 3.14159 }); // "3.14159" - at least 2, unlimited max

Significant Digits (prefix with @):

formatter.FormatMessage("{n, number, ::@@@}", new { n = 12345 });  // "12,300" - exactly 3 significant digits
formatter.FormatMessage("{n, number, ::@@##}", new { n = 1.5 });   // "1.5" - 2 to 4 significant digits

Integer Digits (minimum digits with leading zeros):

formatter.FormatMessage("{n, number, ::000}", new { n = 5 });      // "005" - minimum 3 integer digits
formatter.FormatMessage("{n, number, ::integer-width/*000}", new { n = 5 }); // "005" - alternative form

Notation Styles

// Compact notation
formatter.FormatMessage("{n, number, ::K}", new { n = 1500 });           // "2K" - compact-short
formatter.FormatMessage("{n, number, ::KK}", new { n = 1500 });          // "2 thousand" - compact-long
formatter.FormatMessage("{n, number, ::compact-short}", new { n = 1500000 }); // "2M"
formatter.FormatMessage("{n, number, ::compact-long}", new { n = 1500000 });  // "2 million"

// Scientific notation
formatter.FormatMessage("{n, number, ::scientific}", new { n = 12345 }); // "1.23E+4"

// Engineering notation (exponents in multiples of 3)
formatter.FormatMessage("{n, number, ::engineering}", new { n = 12345 }); // "12.35E+3"

Sign Display

Verbose Concise Description
sign-always +! Always show sign (+42, -42)
sign-never +_ Never show sign (42 for both positive and negative)
sign-except-zero +? Show sign except for zero
sign-accounting () Accounting format for negatives: (100)
sign-accounting-always - Always show sign in accounting format
formatter.FormatMessage("{n, number, ::sign-always}", new { n = 42 });    // "+42"
formatter.FormatMessage("{n, number, ::+!}", new { n = 42 });             // "+42"
formatter.FormatMessage("{n, number, ::sign-accounting}", new { n = -100 }); // "(100)"
formatter.FormatMessage("{n, number, ::()}", new { n = -100 });           // "(100)"

Grouping (Thousands Separators)

Verbose Concise Description
group-off ,_ No grouping separators
group-min2 ,? Group only when 2+ digits in group
group-auto - Automatic grouping (default)
group-always ,! Always apply grouping
formatter.FormatMessage("{n, number, ::group-off}", new { n = 1234567 });  // "1234567"
formatter.FormatMessage("{n, number, ::,_}", new { n = 1234567 });         // "1234567"
formatter.FormatMessage("{n, number, ::group-always}", new { n = 1234567 }); // "1,234,567"

Currency

formatter.FormatMessage("{n, number, ::currency/USD}", new { n = 99.99 }); // "$99.99"
formatter.FormatMessage("{n, number, ::currency/EUR}", new { n = 50 });    // "€50.00"

// Currency display options
formatter.FormatMessage("{n, number, ::currency/USD unit-width-iso-code}", new { n = 100 }); // "USD 100"
formatter.FormatMessage("{n, number, ::currency/USD unit-width-full-name}", new { n = 100 }); // "100 US dollars"
formatter.FormatMessage("{n, number, ::currency/USD currency-narrow-symbol}", new { n = 100 }); // Narrow symbol variant

Units

Note: Requires optional Rh.MessageFormat.CldrData.Units package for locale-specific patterns.

formatter.FormatMessage("{n, number, ::unit/meter}", new { n = 5 });                      // "5 m"
formatter.FormatMessage("{n, number, ::unit/meter unit-width-full-name}", new { n = 5 }); // "5 meters"

Percent and Permille

formatter.FormatMessage("{n, number, ::percent}", new { n = 0.1234 });    // "12%" (multiplies by 100)
formatter.FormatMessage("{n, number, ::%}", new { n = 0.1234 });          // "12%" (concise form)
formatter.FormatMessage("{n, number, ::percent .00}", new { n = 0.1234 }); // "12.34%" (with precision)
formatter.FormatMessage("{n, number, ::permille}", new { n = 0.005 });    // "5‰" (multiplies by 1000)

Ordinal Numbers

Format numbers with locale-appropriate ordinal suffixes (1st, 2nd, 3rd, etc.):

formatter.FormatMessage("{n, number, ::ordinal}", new { n = 1 });   // "1st"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 2 });   // "2nd"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 3 });   // "3rd"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 4 });   // "4th"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 21 });  // "21st"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 22 });  // "22nd"
formatter.FormatMessage("{n, number, ::ordinal}", new { n = 23 });  // "23rd"

// Ordinal suffixes are locale-aware (using CLDR data)
var deFormatter = new MessageFormatter("de-DE");
deFormatter.FormatMessage("{n, number, ::ordinal}", new { n = 1 }); // "1."

var frFormatter = new MessageFormatter("fr-FR");
frFormatter.FormatMessage("{n, number, ::ordinal}", new { n = 1 }); // "1er"
frFormatter.FormatMessage("{n, number, ::ordinal}", new { n = 2 }); // "2e"

Compact Currency

Combine currency formatting with compact notation:

// Compact short currency
formatter.FormatMessage("{n, number, ::currency/USD compact-short}", new { n = 1500000 }); // "$1.5M"
formatter.FormatMessage("{n, number, ::currency/USD K}", new { n = 1000 });                 // "$1K"

// Compact long currency
formatter.FormatMessage("{n, number, ::currency/USD compact-long}", new { n = 1000000 });  // "$1 million"
formatter.FormatMessage("{n, number, ::currency/USD KK}", new { n = 1000000000 });         // "$1 billion"

// With other currencies
formatter.FormatMessage("{n, number, ::currency/EUR compact-short}", new { n = 2500000 }); // "€2.5M"
formatter.FormatMessage("{n, number, ::currency/GBP compact-short}", new { n = 1000 });    // "£1K"

Scale

formatter.FormatMessage("{n, number, ::scale/100}", new { n = 0.5 });     // "50" (multiplies by 100)
formatter.FormatMessage("{n, number, ::scale/1000}", new { n = 1.5 });    // "1,500"

Combining Options

Multiple skeleton tokens can be combined (space-separated):

formatter.FormatMessage("{n, number, ::currency/USD sign-always}", new { n = 100 }); // "+$100.00"
formatter.FormatMessage("{n, number, ::percent .00}", new { n = 0.1234 });           // "12.34%"
formatter.FormatMessage("{n, number, ::compact-short .0}", new { n = 1234567 });     // "1.2M"
formatter.FormatMessage("{n, number, ::scale/1000 group-auto}", new { n = 1.5 });    // "1,500"

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

Note: Requires optional Rh.MessageFormat.CldrData.Lists package for locale-specific patterns.

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

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

Duration Formatting

Format durations from seconds, TimeSpan, or ISO 8601 duration strings:

// Timer format (HH:MM:SS)
formatter.FormatMessage("{d, duration, timer}", new { d = 3661 }); // "1:01:01"

// Long format (words)
formatter.FormatMessage("{d, duration, long}", new { d = 3661 });  // "1 hour 1 minute 1 second"

// Short format
formatter.FormatMessage("{d, duration, short}", new { d = 3661 }); // "1 hr 1 min 1 sec"

// Narrow format
formatter.FormatMessage("{d, duration, narrow}", new { d = 3661 }); // "1h 1m 1s"

// From TimeSpan
var timeSpan = TimeSpan.FromHours(1) + TimeSpan.FromMinutes(30);
formatter.FormatMessage("{d, duration, timer}", new { d = timeSpan }); // "1:30:00"

// From ISO 8601 duration string
formatter.FormatMessage("{d, duration, timer}", new { d = "PT1H30M" }); // "1:30:00"

Number Range Formatting

Format number ranges with locale-appropriate separators:

// Basic range
formatter.FormatMessage("{min, numberRange, max}", new { min = 1, max = 10 });
// Result: "1–10" (with en-dash)

// With currency skeleton
formatter.FormatMessage("{min, numberRange, max, ::currency/USD}", new { min = 100, max = 500 });
// Result: "$100.00–$500.00"

// With percent skeleton
formatter.FormatMessage("{min, numberRange, max, ::%}", new { min = 0.1, max = 0.5 });
// Result: "10%–50%"

// With compact notation
formatter.FormatMessage("{min, numberRange, max, ::compact-short}", new { min = 1000, max = 5000 });
// Result: "1K–5K"

// In context
formatter.FormatMessage("Price range: {min, numberRange, max, ::currency/USD}", new { min = 10, max = 50 });
// Result: "Price range: $10.00–$50.00"

Spellout (Number to Words)

Convert numbers to written words using CLDR Rule-Based Number Format (RBNF) data. This feature requires the optional Rh.MessageFormat.CldrData.Spellout package.

Installation:

dotnet add package Rh.MessageFormat.CldrData.Spellout

Basic Usage:

// Cardinal numbers (default)
formatter.FormatMessage("{n, spellout}", new { n = 42 });
// Result: "forty-two"

formatter.FormatMessage("{n, spellout, cardinal}", new { n = 123 });
// Result: "one hundred twenty-three"

// Ordinal numbers
formatter.FormatMessage("{n, spellout, ordinal}", new { n = 1 });
// Result: "first"

formatter.FormatMessage("{n, spellout, ordinal}", new { n = 21 });
// Result: "twenty-first"

// Year formatting
formatter.FormatMessage("{n, spellout, year}", new { n = 2000 });
// Result: "two thousand"

In Context:

formatter.FormatMessage("You have {count, spellout} items", new { count = 3 });
// Result: "You have three items"

formatter.FormatMessage("This is your {position, spellout, ordinal} visit", new { position = 5 });
// Result: "This is your fifth visit"

Locale Support:

Spellout rules are locale-specific. The package includes RBNF data for 80+ locales:

var deFormatter = new MessageFormatter("de");
deFormatter.FormatMessage("{n, spellout}", new { n = 42 });
// Result: "zwei­und­vierzig"

var frFormatter = new MessageFormatter("fr");
frFormatter.FormatMessage("{n, spellout}", new { n = 21 });
// Result: "vingt-et-un"

Without the Spellout Package:

If the spellout package is not installed, the formatter falls back to standard numeric formatting:

// Without Rh.MessageFormat.CldrData.Spellout installed:
formatter.FormatMessage("{n, spellout}", new { n = 42 });
// Result: "42" (falls back to number formatting)

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"

Passing Variables

Variables can be passed using either anonymous objects or dictionaries:

// Using anonymous objects (recommended for simplicity)
formatter.FormatMessage("Hello {name}, you have {count} messages", new { name = "John", count = 5 });

// Using dictionaries (for dynamic keys or complex scenarios)
var args = new Dictionary<string, object?> { ["name"] = "John", ["count"] = 5 };
formatter.FormatMessage("Hello {name}, you have {count} messages", args);

// Without variables (static text)
formatter.FormatMessage("Hello World", (object?)null);

All formatting methods support both overloads:

  • FormatMessage(string pattern, object? args = null) - accepts anonymous types, POCOs, or null
  • FormatMessage(string pattern, IReadOnlyDictionary<string, object?> args) - accepts dictionaries
  • FormatComplexMessage(string pattern, object? values = null) - accepts anonymous types, POCOs, or null
  • FormatComplexMessage(string pattern, IReadOnlyDictionary<string, object?> values) - accepts dictionaries
  • FormatHtmlMessage(string pattern, object? values = null) - accepts anonymous types, POCOs, or null
  • FormatHtmlMessage(string pattern, IReadOnlyDictionary<string, object?> values) - accepts dictionaries

FormatComplexMessage - Nested Object Support

For nested objects, use FormatComplexMessage which flattens nested structures using __ (double underscore) as separator:

// Nested anonymous objects
formatter.FormatComplexMessage(
    "Hello {user__firstName} {user__lastName}!",
    new { user = new { firstName = "John", lastName = "Doe" } }
);
// Result: "Hello John Doe!"

// Deeply nested structures
formatter.FormatComplexMessage(
    "City: {address__home__city}",
    new { address = new { home = new { city = "New York" } } }
);
// Result: "City: New York"

FormatHtmlMessage - Safe HTML Formatting

For messages containing HTML markup, use FormatHtmlMessage which HTML-escapes variable values to prevent XSS:

formatter.FormatHtmlMessage(
    "<b>Hello {name}</b>",
    new { name = "<script>alert('xss')</script>" }
);
// Result: "<b>Hello &lt;script&gt;alert('xss')&lt;/script&gt;</b>"

Configuration

MessageFormatterOptions

var options = new MessageFormatterOptions
{
    // IMPORTANT: DefaultFallbackLocale is null by default.
    // Set this to enable fallback when exact locale data is not found.
    DefaultFallbackLocale = "en",
    CldrDataProvider = new CldrDataProvider(),  // CLDR data source
    CultureInfoCache = new CultureInfoCache()   // CultureInfo caching
};

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

Note: DefaultFallbackLocale is null by default. If not set and the requested locale is not found, an InvalidLocaleException will be thrown. Set this property to enable automatic fallback to a default locale.

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: null)

Important: DefaultFallbackLocale is null by default. You must explicitly set it to enable fallback behavior. If no locale can be resolved and no fallback is configured, an InvalidLocaleException is thrown.

// Configure fallback locale
var options = new MessageFormatterOptions
{
    DefaultFallbackLocale = "en"  // Required for fallback behavior
};

// 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/           # Core CLDR locale data (plurals, ordinals, currencies, dates)
├── Rh.MessageFormat.CldrData.Spellout/  # Optional: RBNF spellout data
├── Rh.MessageFormat.CldrData.Units/     # Optional: Unit formatting patterns
├── Rh.MessageFormat.CldrData.Lists/     # Optional: List formatting patterns
├── Rh.MessageFormat.CldrData.RelativeTime/ # Optional: Relative time patterns
├── Rh.MessageFormat.CldrData.DateRange/ # Optional: Date range/interval patterns
├── 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

Core 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 (plurals, ordinals, currencies, dates)

Optional Packages

Optional packages provide additional CLDR data features. They auto-register via ModuleInitializer when referenced - no configuration needed. If not installed, the formatter gracefully falls back to default behavior.

Package Description
Rh.MessageFormat.CldrData.Spellout RBNF spellout data for number-to-words conversion
Rh.MessageFormat.CldrData.Units Unit formatting patterns (length, duration, mass, etc.)
Rh.MessageFormat.CldrData.Lists List formatting patterns (conjunction, disjunction, unit)
Rh.MessageFormat.CldrData.RelativeTime Relative time patterns (yesterday, in 3 days, etc.)
Rh.MessageFormat.CldrData.DateRange Date interval/range formatting patterns

Installation example:

dotnet add package Rh.MessageFormat                      # Core (required)
dotnet add package Rh.MessageFormat.CldrData.Spellout    # Optional: number-to-words
dotnet add package Rh.MessageFormat.CldrData.Units       # Optional: unit formatting

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

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages