Rh.MessageFormat.CldrData
1.2.0
dotnet add package Rh.MessageFormat.CldrData --version 1.2.0
NuGet\Install-Package Rh.MessageFormat.CldrData -Version 1.2.0
<PackageReference Include="Rh.MessageFormat.CldrData" Version="1.2.0" />
<PackageVersion Include="Rh.MessageFormat.CldrData" Version="1.2.0" />
<PackageReference Include="Rh.MessageFormat.CldrData" />
paket add Rh.MessageFormat.CldrData --version 1.2.0
#r "nuget: Rh.MessageFormat.CldrData, 1.2.0"
#:package Rh.MessageFormat.CldrData@1.2.0
#addin nuget:?package=Rh.MessageFormat.CldrData&version=1.2.0
#tool nuget:?package=Rh.MessageFormat.CldrData&version=1.2.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");
All formatting methods support these overloads:
FormatMessage<T>(string pattern, T args)- accepts anonymous types, POCOs, records, or value tuples; safe for trimming and Native AOTFormatMessage(string pattern, IReadOnlyDictionary<string, object?> args)- accepts dictionariesFormatMessage(string pattern)- static text without variablesFormatMessage(string pattern, object? args = null)- accepts any object; not safe for trimming/Native AOT (see below)
FormatComplexMessage and FormatHtmlMessage provide the same set of overloads.
Trimming and Native AOT
The generic overloads (FormatMessage<T>, FormatComplexMessage<T>, FormatHtmlMessage<T>) are
annotated with [DynamicallyAccessedMembers], so the trimmer preserves the public properties and
fields of the argument type at each call site. They bind automatically whenever the compiler knows
the static type of the argument:
// Trim/AOT safe - T is inferred as the anonymous type / POCO / record
formatter.FormatMessage("Hello {FirstName}", new { FirstName = "Mike" });
formatter.FormatMessage("Hello {FirstName}", new Person { FirstName = "Mike" });
The object?-based overloads are marked [RequiresUnreferencedCode]: when the argument is only
statically known as object, the trimmer cannot tell which members to preserve, and trimmed or
Native AOT builds emit an IL2026 warning at the call site. In that situation pass a
IReadOnlyDictionary<string, object?> instead.
Two limitations to be aware of in trimmed/Native AOT applications:
- Only the members of the argument type itself are statically preserved. Members of nested
complex objects may still be trimmed - use nested dictionaries with
FormatComplexMessagefor nested values. - Value tuple element names are erased at runtime, so a tuple's values are exposed as
{Item1},{Item2}, etc.
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.Abstractions (>= 1.2.0)
-
net8.0
- Rh.MessageFormat.Abstractions (>= 1.2.0)
NuGet packages (6)
Showing the top 5 NuGet packages that depend on Rh.MessageFormat.CldrData:
| Package | Downloads |
|---|---|
|
Rh.MessageFormat
Package Description |
|
|
Rh.MessageFormat.CldrData.Units
CLDR unit display data for Rh.MessageFormat. Optional package for unit formatting (e.g., "5 kilometers", "3 hours"). |
|
|
Rh.MessageFormat.CldrData.Spellout
CLDR RBNF spellout data for Rh.MessageFormat. Optional package for number-to-words conversion. |
|
|
Rh.MessageFormat.CldrData.DateRange
CLDR date range/interval data for Rh.MessageFormat. Optional package for date range formatting (e.g., "Jan 5 - 15, 2024"). |
|
|
Rh.MessageFormat.CldrData.Lists
CLDR list pattern data for Rh.MessageFormat. Optional package for list formatting (e.g., "A, B, and C"). |
GitHub repositories
This package is not used by any popular GitHub repositories.