docs/extensibility.md
Humanizer is designed to be extensible, allowing you to customize and extend its behavior to meet your specific needs.
Implement IStringTransformer to create custom string transformations:
public interface IStringTransformer
{
string Transform(string input);
}
public class MyTitleCase : IStringTransformer
{
private readonly HashSet<string> _articlesAndPrepositions = new()
{
"a", "an", "the", "and", "but", "or", "for", "nor", "on", "at", "to", "from", "by"
};
public string Transform(string input)
{
var words = input.Split(' ');
for (int i = 0; i < words.Length; i++)
{
// Always capitalize first word
if (i == 0 || !_articlesAndPrepositions.Contains(words[i].ToLower()))
{
words[i] = char.ToUpper(words[i][0]) + words[i].Substring(1).ToLower();
}
}
return string.Join(" ", words);
}
}
// Usage
"the quick brown fox".Transform(new MyTitleCase());
// => "The Quick Brown Fox"
The advantage over the built-in LetterCasing enum is complete control over the transformation logic.
Implement ITruncator to create custom truncation strategies:
public interface ITruncator
{
string Truncate(string value, int length, string truncationString,
TruncateFrom truncateFrom = TruncateFrom.Right);
}
public class SentenceTruncator : ITruncator
{
public string Truncate(string value, int length, string truncationString,
TruncateFrom truncateFrom)
{
if (value.Length <= length)
return value;
// Find the last sentence boundary before the length limit
var truncated = value.Substring(0, length);
var lastPeriod = truncated.LastIndexOf('.');
if (lastPeriod > 0)
{
return truncated.Substring(0, lastPeriod + 1);
}
// Fall back to word boundary
var lastSpace = truncated.LastIndexOf(' ');
if (lastSpace > 0)
{
return truncated.Substring(0, lastSpace) + truncationString;
}
return truncated + truncationString;
}
}
// Usage
"First sentence. Second sentence. Third sentence."
.Truncate(30, "...", new SentenceTruncator());
// => "First sentence."
Extend the pluralization/singularization vocabulary:
// Add irregular word
Vocabularies.Default.AddIrregular("person", "people");
// Add uncountable word
Vocabularies.Default.AddUncountable("equipment");
// Add plural rule
Vocabularies.Default.AddPlural("(quiz)$", "$1zes");
// Add singular rule
Vocabularies.Default.AddSingular("(vert|ind)ices$", "$1ex");
// Match only "person" (not "salesperson")
Vocabularies.Default.AddIrregular("person", "people", matchEnding: false);
// Match "person" and "salesperson"
Vocabularies.Default.AddIrregular("person", "people", matchEnding: true);
Implement language-specific number to words conversion:
public interface INumberToWordsConverter
{
string Convert(long number);
string Convert(long number, GrammaticalGender gender);
string ConvertToOrdinal(int number);
string ConvertToOrdinal(int number, GrammaticalGender gender);
}
Register your converter:
Configurator.NumberToWordsConverters.Register("my-culture",
new MyNumberToWordsConverter());
Implement custom date/time/number formatting:
public interface IFormatter
{
string DateHumanize(DateTime value, DateTime? comparisonBase,
CultureInfo culture);
string TimeSpanHumanize(TimeSpan timeSpan, int precision,
CultureInfo culture);
// ... other methods
}
Register your formatter:
Configurator.Formatters.Register("my-culture", new MyFormatter());
Customize Humanizer's behavior globally:
// Custom enum description property name
Configurator.EnumDescriptionPropertyLocator = p => p.Name == "Info";
// Custom collection separator
Configurator.CollectionFormatters.Register("my-culture",
new MyCollectionFormatter());
Implement interfaces rather than extending classes - This ensures compatibility with future versions
Register custom components at application startup - Do this once rather than on every use
Thread safety - Make custom implementations thread-safe if they'll be used concurrently
Test thoroughly - Custom implementations should have comprehensive test coverage
Consider culture - If your custom component is culture-specific, register it appropriately