Rh.MessageFormat.CldrData.RelativeTime
1.0.0
See the version list below for details.
dotnet add package Rh.MessageFormat.CldrData.RelativeTime --version 1.0.0
NuGet\Install-Package Rh.MessageFormat.CldrData.RelativeTime -Version 1.0.0
<PackageReference Include="Rh.MessageFormat.CldrData.RelativeTime" Version="1.0.0" />
<PackageVersion Include="Rh.MessageFormat.CldrData.RelativeTime" Version="1.0.0" />
<PackageReference Include="Rh.MessageFormat.CldrData.RelativeTime" />
paket add Rh.MessageFormat.CldrData.RelativeTime --version 1.0.0
#r "nuget: Rh.MessageFormat.CldrData.RelativeTime, 1.0.0"
#:package Rh.MessageFormat.CldrData.RelativeTime@1.0.0
#addin nuget:?package=Rh.MessageFormat.CldrData.RelativeTime&version=1.0.0
#tool nuget:?package=Rh.MessageFormat.CldrData.RelativeTime&version=1.0.0
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, 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.Unitspackage 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.Listspackage 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: "zweiundvierzig"
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 nullFormatMessage(string pattern, IReadOnlyDictionary<string, object?> args)- accepts dictionariesFormatComplexMessage(string pattern, object? values = null)- accepts anonymous types, POCOs, or nullFormatComplexMessage(string pattern, IReadOnlyDictionary<string, object?> values)- accepts dictionariesFormatHtmlMessage(string pattern, object? values = null)- accepts anonymous types, POCOs, or nullFormatHtmlMessage(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 <script>alert('xss')</script></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:
DefaultFallbackLocaleisnullby default. If not set and the requested locale is not found, anInvalidLocaleExceptionwill 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:
- Exact match - e.g.,
en-GB - Base locale - e.g.,
en-GB→en - Default fallback - Configured via
DefaultFallbackLocale(default:null)
Important:
DefaultFallbackLocaleisnullby default. You must explicitly set it to enable fallback behavior. If no locale can be resolved and no fallback is configured, anInvalidLocaleExceptionis 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:
- 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/ # 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
| 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
- Rh.MessageFormat.CldrData (>= 1.0.0)
-
net8.0
- Rh.MessageFormat.CldrData (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.