Localization and extensibility
Start with culture selection, then choose the narrowest extension point. Pass CultureInfo per call when possible. Establish CurrentCulture and CurrentUICulture at a request or job boundary for ambient APIs. Use a local interface implementation for one operation and reserve Configurator registries or strategies for process-wide application policy.
Example
This example produces French number words and supplies a per-call culture-aware transformer:
using System.Globalization;
using Humanizer;
var french = CultureInfo.GetCultureInfo("fr-FR");
CultureInfo.CurrentCulture = french;
CultureInfo.CurrentUICulture = french;
Console.WriteLine($"Number: {42.ToWords(french)}");
Console.WriteLine($"Transformed: {"bonjour".Transform(french, new LoudTransformer())}");
sealed class LoudTransformer : ICulturedStringTransformer
{
public string Transform(string input) =>
Transform(input, CultureInfo.CurrentCulture);
public string Transform(string input, CultureInfo culture) =>
culture.TextInfo.ToUpper(input);
}
Number: quarante-deux
Transformed: BONJOUR
A per-call transformer is isolated and easy to test.
Choose an extension point
| Need | Preferred extension |
|---|---|
| One local string operation | IStringTransformer or ICulturedStringTransformer |
| A custom truncation rule | ITruncator passed to Truncate |
| Relative date thresholds | A date/time strategy assigned once |
| Application-wide locale component | The matching LocaliserRegistry<T> |
| Case-aware duration policy | IGrammaticalCaseTimeSpanHumanizeStrategy and IGrammaticalCaseTimeSpanFormatter |
| Enum metadata property | UseEnumDescriptionPropertyLocator at startup |
Registries cover collection formatters, general formatters, number-word converters, ordinalizers, date-ordinal converters, and clock-notation converters. Built-in registries resolve exact accepted-culture names through the generated map before using the registry default. Caller-created registries retain parent-chain fallback.
Set a global time-span policy
Assign Configurator.TimeSpanHumanizeStrategy once during
application startup to route every TimeSpan.Humanize and
HumanizeToSymbols call through an application-wide policy. Fractional-second
calls use IFractionalTimeSpanHumanizeStrategy when the configured strategy
implements it; integral results remain compatible with legacy strategies.
With the default strategy, genuinely fractional terminal values require
IFractionalTimeSpanFormatter from a configured custom formatter. A custom
IFractionalTimeSpanHumanizeStrategy may own its fractional formatting.
Humanizer does not fall back to English. The fractional strategy receives the
requested precision, empty-unit behavior, culture, unit range, separator,
maximum fractional digits, rounding mode, and symbol mode.
This runnable configuration keeps the built-in date strategy example and adds
a custom time-span strategy that delegates all behavior to
DefaultTimeSpanHumanizeStrategy after limiting output to one part:
using System.Globalization;
using Humanizer;
var culture = CultureInfo.GetCultureInfo("en-US");
var comparison = new DateTime(2025, 1, 20, 12, 0, 0, DateTimeKind.Utc);
Configurator.DateTimeHumanizeStrategy =
new PrecisionDateTimeHumanizeStrategy(precision: 0.75);
Configurator.TimeSpanHumanizeStrategy =
new SingleUnitTimeSpanHumanizeStrategy();
var result = comparison.AddMinutes(-45).Humanize(
utcDate: true,
dateToCompareAgainst: comparison,
culture: culture);
var duration = TimeSpan.FromMinutes(62).Humanize(
precision: 2,
culture: culture);
Console.WriteLine($"{result}; {duration}");
sealed class SingleUnitTimeSpanHumanizeStrategy : ITimeSpanHumanizeStrategy
{
readonly DefaultTimeSpanHumanizeStrategy defaultStrategy = new();
public string Humanize(
TimeSpan timeSpan,
int precision,
bool countEmptyUnits,
CultureInfo? culture,
TimeUnit maxUnit,
TimeUnit minUnit,
string? collectionSeparator,
bool toWords,
bool toSymbols) =>
defaultStrategy.Humanize(
timeSpan,
Math.Min(precision, 1),
countEmptyUnits,
culture,
maxUnit,
minUnit,
collectionSeparator,
toWords,
toSymbols);
}
an hour ago; 1 hour
HumanizeWithCase uses a separate optional capability so existing custom
components remain source- and binary-compatible. A configured strategy must
implement IGrammaticalCaseTimeSpanHumanizeStrategy, and its selected
formatter must implement IGrammaticalCaseTimeSpanFormatter. Otherwise the
case-aware call throws NotSupportedException, including when the requested
case is Nominative. Existing Humanize and HumanizeToSymbols calls
continue to use ITimeSpanHumanizeStrategy and IFormatter unchanged.
The case-aware formatter receives the duration unit, numeric count, and grammatical case. It returns a complete locale-authored unit-case phrase: singular output may contain a localized one-word or article, while a counted form may render the count explicitly or encode it in the unit form. It must not add a preposition. Install custom strategies and formatters during startup, before registry resolution freezes.
Register global behavior before first use
Localizer registries freeze on first resolution. Register components before any Humanizer call can resolve that registry. Do not mutate global strategies per request; concurrent callers can observe the change.
Parent-culture fallback identifies where behavior was resolved. It does not prove that inherited wording is correct for every region.
Related guides and API
- Configuration basics
- Choose explicit and ambient cultures
- Custom localization
- Culture and global configuration
- Configurator API
- LocaliserRegistry API
- ICulturedStringTransformer API
- ITruncator API
- ITimeSpanHumanizeStrategy API
- IGrammaticalCaseTimeSpanHumanizeStrategy API
- IGrammaticalCaseTimeSpanFormatter API
- DefaultTimeSpanHumanizeStrategy API