Skip to main content
Version: 4.0 (next)

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:

Program.cs
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);
}
Output
Number: quarante-deux
Transformed: BONJOUR

A per-call transformer is isolated and easy to test.

Choose an extension point​

NeedPreferred extension
One local string operationIStringTransformer or ICulturedStringTransformer
A custom truncation ruleITruncator passed to Truncate
Relative date thresholdsA date/time strategy assigned once
Application-wide locale componentThe matching LocaliserRegistry<T>
Case-aware duration policyIGrammaticalCaseTimeSpanHumanizeStrategy and IGrammaticalCaseTimeSpanFormatter
Enum metadata propertyUseEnumDescriptionPropertyLocator 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:

Program.cs
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);
}
Output
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.