Humanizer: людиномовні рядки у .NET
Humanizer: людиномовні рядки у .NET
Є клас задач, про які розробники рідко думають наперед, і якими потім займаються в останній момент: «Як написати '23 хвилини тому' замість 1716312000?», «Як перетворити UserProfileSettings на User profile settings для заголовку?», «Як написати 1 файл vs 5 файлів правильно?»
Humanizer вирішує саме ці задачі. Це бібліотека з відкритим кодом для .NET, що перетворює типи .NET — TimeSpan, DateTime, int, Enum, рядки — у зрозумілі людині форми. Замість написання власної логіки відмінювань, кейс-конвертацій і форматувань — підключаєте один NuGet та пишете виразний, читабельний код.
Humanizer підтримує 67 мов, включаючи українську. Це означає: 2.Hours().Humanize(culture: new CultureInfo("uk")) поверне "2 години", а DateTime.UtcNow.AddDays(-3).Humanize(culture: new CultureInfo("uk")) — "3 дні тому".
IStringLocalizer, а доповнення: він береться за читабельне форматування типів .NET, тоді як IStringLocalizer відповідає за переклад бізнес-текстів.Встановлення
dotnet add package Humanizer
Для конкретних мов можна встановити окремий пакет з лише потрібними ресурсами (менший розмір):
dotnet add package Humanizer.Core # Тільки Humanizer без перекладів
dotnet add package Humanizer # Humanizer + всі 67 мов
Після встановлення жодної реєстрації у DI не потрібно — всі методи Humanizer є методами розширення і доступні після using Humanizer;.
Частина 1. Humanize(): коли було і коли буде
DateTime.Humanize() — відносний час
Найпопулярніша функція Humanizer: перетворення DateTime у читабельний рядок відносного часу.
using Humanizer;
using System.Globalization;
// ─── Минулий час ───────────────────────────────────────────────
DateTime.UtcNow.AddSeconds(-30).Humanize() // "30 seconds ago"
DateTime.UtcNow.AddMinutes(-2).Humanize() // "2 minutes ago"
DateTime.UtcNow.AddHours(-1).Humanize() // "an hour ago"
DateTime.UtcNow.AddHours(-5).Humanize() // "5 hours ago"
DateTime.UtcNow.AddDays(-1).Humanize() // "yesterday"
DateTime.UtcNow.AddDays(-3).Humanize() // "3 days ago"
DateTime.UtcNow.AddMonths(-2).Humanize() // "2 months ago"
DateTime.UtcNow.AddYears(-1).Humanize() // "one year ago"
DateTime.UtcNow.AddYears(-5).Humanize() // "5 years ago"
// ─── Майбутній час ─────────────────────────────────────────────
DateTime.UtcNow.AddSeconds(45).Humanize() // "45 seconds from now"
DateTime.UtcNow.AddMinutes(10).Humanize() // "10 minutes from now"
DateTime.UtcNow.AddHours(3).Humanize() // "3 hours from now"
DateTime.UtcNow.AddDays(1).Humanize() // "tomorrow"
DateTime.UtcNow.AddDays(7).Humanize() // "7 days from now"
// ─── Українська мова ───────────────────────────────────────────
var uk = new CultureInfo("uk");
DateTime.UtcNow.AddMinutes(-2).Humanize(culture: uk) // "2 хвилини тому"
DateTime.UtcNow.AddHours(-1).Humanize(culture: uk) // "годину тому"
DateTime.UtcNow.AddDays(-1).Humanize(culture: uk) // "вчора"
DateTime.UtcNow.AddDays(-5).Humanize(culture: uk) // "5 днів тому"
DateTime.UtcNow.AddMonths(-1).Humanize(culture: uk) // "місяць тому"
// ─── Польська мова ─────────────────────────────────────────────
var pl = new CultureInfo("pl");
DateTime.UtcNow.AddMinutes(-2).Humanize(culture: pl) // "2 minuty temu"
DateTime.UtcNow.AddDays(-3).Humanize(culture: pl) // "3 dni temu"
Humanizer інтелектуально обирає форму: "an hour ago" замість "1 hour ago", "yesterday" замість "1 day ago". З параметром culture він додатково застосовує правила відмінювання мови.
DateTime.Humanize() з власною точкою відліку
За замовчуванням порівняння відбувається відносно DateTime.UtcNow. Але можна задати власну точку відліку:
var publishedAt = new DateTime(2024, 01, 15, 12, 0, 0, DateTimeKind.Utc);
var comparedTo = new DateTime(2024, 01, 20, 12, 0, 0, DateTimeKind.Utc);
publishedAt.Humanize(dateToCompareAgainst: comparedTo) // "5 days ago"
// (відносно comparedTo, а не DateTime.UtcNow)
Це корисно для стрічок активності де кожен запис відображається відносно певного контексту, а не поточного часу.
TimeSpan.Humanize() — тривалості
Для форматування тривалості використовується TimeSpan.Humanize():
TimeSpan.FromSeconds(1).Humanize() // "1 second"
TimeSpan.FromSeconds(90).Humanize() // "1 minute"
TimeSpan.FromMinutes(5).Humanize() // "5 minutes"
TimeSpan.FromHours(2).Humanize() // "2 hours"
TimeSpan.FromDays(1).Humanize() // "1 day"
TimeSpan.FromDays(30).Humanize() // "4 weeks"
// Деталізація через параметр precision (скільки одиниць показувати)
TimeSpan.FromMinutes(95).Humanize(precision: 1) // "1 hour"
TimeSpan.FromMinutes(95).Humanize(precision: 2) // "1 hour, 35 minutes"
TimeSpan.FromHours(26).Humanize(precision: 1) // "1 day"
TimeSpan.FromHours(26).Humanize(precision: 2) // "1 day, 2 hours"
// Максимальна деталізація
TimeSpan.FromMinutes(125).Humanize(precision: 3) // "2 hours, 5 minutes"
// Українська — правильне відмінювання
var uk = new CultureInfo("uk");
TimeSpan.FromMinutes(5).Humanize(culture: uk) // "5 хвилин"
TimeSpan.FromMinutes(1).Humanize(culture: uk) // "1 хвилина"
TimeSpan.FromMinutes(21).Humanize(culture: uk) // "21 хвилина"
TimeSpan.FromHours(3).Humanize(culture: uk) // "3 години"
TimeSpan.FromHours(11).Humanize(culture: uk) // "11 годин"
TimeSpan.FromDays(1).Humanize(culture: uk) // "1 день"
TimeSpan.FromDays(5).Humanize(culture: uk) // "5 днів"
TimeSpan.FromDays(2).Humanize(precision: 2, culture: uk) // "2 дні, 0 годин"
Відмінювання 1 хвилина / 2 хвилини / 5 хвилин — класична задача для Humanizer. Без нього це десятки умов у коді.
TimeUnit.Humanize() — конвертер одиниць часу
// Fluent API для побудови TimeSpan
2.Hours() // TimeSpan.FromHours(2)
30.Minutes() // TimeSpan.FromMinutes(30)
1.Days() // TimeSpan.FromDays(1)
// Використання у поєднанні:
var deadline = DateTime.UtcNow + 2.Hours() + 30.Minutes();
// Арифметика дат
var nextWeek = DateTime.UtcNow + 7.Days();
var lastMonth = DateTime.UtcNow - 1.Months();
var inTwoYears = DateTime.UtcNow + 2.Years();
Ці методи роблять код більш виразним: 2.Hours() читається значно краще ніж TimeSpan.FromHours(2).
Частина 2. Числа та слова: відмінювання
ToWords() — число словами
1.ToWords() // "one"
10.ToWords() // "ten"
42.ToWords() // "forty-two"
100.ToWords() // "one hundred"
1000.ToWords() // "one thousand"
1_000_000.ToWords() // "one million"
// Від'ємні числа
(-3).ToWords() // "minus three"
// Великі числа
1_234_567.ToWords() // "one million two hundred and thirty-four thousand five hundred and sixty-seven"
// Українська
var uk = new CultureInfo("uk");
1.ToWords(uk) // "один"
5.ToWords(uk) // "п'ять"
21.ToWords(uk) // "двадцять один"
100.ToWords(uk) // "сто"
1000.ToWords(uk) // "одна тисяча"
(-7).ToWords(uk) // "мінус сім"
ToOrdinalWords() — порядкові числівники
1.ToOrdinalWords() // "first"
2.ToOrdinalWords() // "second"
3.ToOrdinalWords() // "third"
10.ToOrdinalWords() // "tenth"
21.ToOrdinalWords() // "twenty-first"
100.ToOrdinalWords() // "one hundredth"
// Українська
var uk = new CultureInfo("uk");
1.ToOrdinalWords(uk) // "перший"
2.ToOrdinalWords(uk) // "другий"
5.ToOrdinalWords(uk) // "п'ятий"
21.ToOrdinalWords(uk) // "двадцять перший"
Ordinalize() — числовий суфікс
// Для en: 1st, 2nd, 3rd, 4th...
1.Ordinalize() // "1st"
2.Ordinalize() // "2nd"
3.Ordinalize() // "3rd"
4.Ordinalize() // "4th"
11.Ordinalize() // "11th" (не "11st"!)
21.Ordinalize() // "21st"
Pluralize() та Singularize() — однина і множина
// Автоматичне визначення множини (тільки англійська)
"file".Pluralize() // "files"
"child".Pluralize() // "children"
"person".Pluralize() // "people"
"mouse".Pluralize() // "mice"
"status".Pluralize() // "statuses"
"category".Pluralize() // "categories"
"files".Singularize() // "file"
"children".Singularize() // "child"
"people".Singularize() // "person"
Для динамічного відмінювання залежно від кількості:
// Format() з відмінюванням
int count = 5;
$"You have {count} {"message".ToQuantity(count)}"
// → "You have 5 messages"
count = 1;
$"You have {count} {"message".ToQuantity(count)}"
// → "You have 1 message"
// ToQuantity — ще зручніший API
"file".ToQuantity(1) // "1 file"
"file".ToQuantity(5) // "5 files"
"file".ToQuantity(0) // "0 files"
// Зі словесним числівником
"file".ToQuantity(2, ShowQuantityAs.Words)
// → "two files"
Частина 3. Рядки: перетворення Case і регістрів
Перетворення між стилями іменування
Humanizer вміє конвертувати рядки між різними стилями іменування — незамінно при побудові API, відображенні полів форм, генерації документації.
// PascalCase / camelCase → читабельний текст
"PascalCaseString".Humanize() // "Pascal case string"
"Underscored_input_string".Humanize() // "Underscored input string"
"HTMLParser".Humanize() // "HTML parser"
"UserProfileSettings".Humanize() // "User profile settings"
"createdAt".Humanize() // "Created at"
// Регістр першого слова
"some title".Humanize(LetterCasing.Title) // "Some Title"
"some title".Humanize(LetterCasing.AllCaps) // "SOME TITLE"
"some title".Humanize(LetterCasing.LowerCase) // "some title"
// ─── Зворотні перетворення ─────────────────────────────────────
// → PascalCase
"some string".Pascalize() // "SomeString"
"some-string".Pascalize() // "SomeString"
// → camelCase
"Some String".Camelize() // "someString"
// → underscore_case (snake_case)
"SomeProperty".Underscore() // "some_property"
"Some string".Underscore() // "some_string"
// → kebab-case
"SomeProperty".Kebaberize() // "some-property"
// → Titleize
"some title here".Titleize() // "Some Title Here"
// → Truncate
"Long string that needs truncating".Truncate(20)
// → "Long string that ne…"
"Long string that needs truncating".Truncate(20, "...")
// → "Long string that..."
Практичне використання
// Відображення назв властивостей у UI
var properties = typeof(UserProfile).GetProperties();
foreach (var prop in properties)
{
var label = prop.Name.Humanize(LetterCasing.Title);
// "FirstName" → "First Name"
// "PhoneNumber" → "Phone Number"
// "IsEmailVerified" → "Is Email Verified"
Console.WriteLine($"{label}: ...");
}
Частина 4. Enum: читабельні значення
Одна з найчастіших задач — відображати значення enum не як "Pending", а як "В очікуванні" або "In Progress" замість "InProgress".
public enum OrderStatus
{
Pending,
InProgress,
WaitingForPayment,
Completed,
Cancelled
}
// Автоматична гуманізація (PascalCase → читабельний рядок)
OrderStatus.Pending.Humanize() // "Pending"
OrderStatus.InProgress.Humanize() // "In progress"
OrderStatus.WaitingForPayment.Humanize() // "Waiting for payment"
OrderStatus.Completed.Humanize() // "Completed"
// Власне відображення через атрибут [Description]
using System.ComponentModel;
public enum OrderStatus
{
[Description("Очікує обробки")]
Pending,
[Description("В обробці")]
InProgress,
[Description("Очікує оплати")]
WaitingForPayment
}
OrderStatus.Pending.Humanize() // "Очікує обробки"
OrderStatus.InProgress.Humanize() // "В обробці"
// DehumanizeTo<T>() — зворотнє перетворення
"Pending".DehumanizeTo<OrderStatus>() // OrderStatus.Pending
"In progress".DehumanizeTo<OrderStatus>() // OrderStatus.InProgress
Атрибут [Description] — це стандартний атрибут з System.ComponentModel. Якщо Humanizer знаходить його — використовує його значення замість назви члена enum. Це ідеальне місце для локалізованих рядків.
Частина 5. ByteSize — розмір файлів
using Humanizer.Bytes;
// Побудова ByteSize
var size = ByteSize.FromBytes(1024); // 1 KiB
var megabytes = ByteSize.FromMegabytes(2.5); // 2.5 MB
var gigabytes = ByteSize.FromGigabytes(1); // 1 GB
// Форматування
ByteSize.FromBytes(0).Humanize() // "0 B"
ByteSize.FromBytes(512).Humanize() // "512 B"
ByteSize.FromBytes(1024).Humanize() // "1 KB"
ByteSize.FromBytes(1_500_000).Humanize() // "1.43 MB"
ByteSize.FromBytes(2_500_000_000).Humanize() // "2.33 GB"
// Власний формат
ByteSize.FromBytes(1_500_000).Humanize("MB") // "1.43 MB"
ByteSize.FromBytes(1_500_000).Humanize("0.0 MB") // "1.4 MB"
// Парсинг
ByteSize.Parse("1.5 GB") // 1,610,612,736 bytes
ByteSize.Parse("500 MB") // 524,288,000 bytes
ByteSize.TryParse("invalid", out _) // false
Практичний приклад — відображення розміру завантаженого файлу:
app.MapPost("/upload", async (IFormFile file) =>
{
var size = ByteSize.FromBytes(file.Length);
return Results.Ok(new
{
fileName = file.FileName,
size = size.Humanize(), // "2.34 MB"
sizeBytes = file.Length
});
});
Частина 6. Інтеграція з ASP.NET Core Minimal API
Endpoint з повним набором Humanizer-функцій
Ось практичний приклад — API для стрічки активності блогу:
using Humanizer;
using System.Globalization;
namespace ShopApi.Features.Activity;
public record BlogPost(
int Id,
string Title,
DateTime PublishedAt,
long SizeBytes,
int ViewCount,
PostStatus Status);
public enum PostStatus
{
[Description("Чернетка")]
Draft,
[Description("На модерації")]
UnderReview,
[Description("Опубліковано")]
Published,
[Description("Архівовано")]
Archived
}
public static class ActivityEndpoints
{
private static readonly List<BlogPost> _posts =
[
new(1, "Вступ до Minimal API",
DateTime.UtcNow.AddMinutes(-45), 12_340, 1250, PostStatus.Published),
new(2, "Humanizer у .NET",
DateTime.UtcNow.AddHours(-3), 8_192, 87, PostStatus.Published),
new(3, "Локалізація у ASP.NET Core",
DateTime.UtcNow.AddDays(-2), 25_600, 312, PostStatus.Published),
new(4, "Entity Framework Core",
DateTime.UtcNow.AddDays(-14), 51_200, 4_029, PostStatus.Archived),
];
public static WebApplication MapActivityEndpoints(this WebApplication app)
{
app.MapGet("/posts", GetPosts).WithTags("Posts");
return app;
}
private static IResult GetPosts(string? lang = "uk")
{
var culture = lang switch
{
"uk" => new CultureInfo("uk"),
"pl" => new CultureInfo("pl"),
_ => new CultureInfo("en"),
};
var result = _posts.Select(post => new
{
post.Id,
post.Title,
// "45 хвилин тому" / "45 minutes ago"
PublishedAgo = post.PublishedAt.Humanize(culture: culture),
// "25.03.2024" — форматована дата за культурою
PublishedDate = post.PublishedAt.ToString("d", culture),
// "12.34 KB" / "25 KB" / "50 KB"
Size = ByteSize.FromBytes(post.SizeBytes).Humanize(),
// "1 250 переглядів" → через ToQuantity (en only для pluralize)
// Для uk — власний формат:
Views = culture.Name.StartsWith("uk")
? $"{post.ViewCount:N0} переглядів"
: $"{"view".ToQuantity(post.ViewCount, ShowQuantityAs.Numeric)}",
// "Опубліковано" (через [Description] атрибут)
Status = post.Status.Humanize(),
// "Published" → зворотньо через Dehumanize
StatusCode = post.Status.ToString(),
}).ToList();
return Results.Ok(result);
}
}
Відповідь для ?lang=uk:
[
{
"id": 1,
"title": "Вступ до Minimal API",
"publishedAgo": "45 хвилин тому",
"publishedDate": "18.03.2024",
"size": "12.06 KB",
"views": "1 250 переглядів",
"status": "Опубліковано",
"statusCode": "Published"
},
{
"id": 4,
"title": "Entity Framework Core",
"publishedAgo": "14 днів тому",
"publishedDate": "04.03.2024",
"size": "50 KB",
"views": "4 029 переглядів",
"status": "Архівовано",
"statusCode": "Archived"
}
]
Інтеграція з RequestLocalizationMiddleware
Замість ручного передавання lang параметра — Humanizer може читати CultureInfo.CurrentCulture, яку вже встановив UseRequestLocalization:
// Замість: post.PublishedAt.Humanize(culture: culture)
// Після того як middleware встановив Thread.CurrentCulture:
post.PublishedAt.Humanize()
// Humanizer автоматично використовує CultureInfo.CurrentCulture
Це ключова перевага: якщо в застосунку вже налаштована локалізація через UseRequestLocalization — Humanizer підхоплює культуру автоматично, без додаткового коду.
Частина 7. Огляд всіх можливостей (шпаргалка)
| Метод | Приклад входу | Приклад виходу |
|---|---|---|
DateTime.Humanize() | DateTime.UtcNow - 2h | "2 hours ago" / "2 години тому" |
TimeSpan.Humanize() | TimeSpan.FromMinutes(95) | "1 hour, 35 minutes" |
int.ToWords() | 42 | "forty-two" / "сорок два" |
int.ToOrdinalWords() | 3 | "third" / "третій" |
int.Ordinalize() | 21 | "21st" |
string.Humanize() | "PascalCase" | "Pascal case" |
string.Pascalize() | "some string" | "SomeString" |
string.Camelize() | "Some String" | "someString" |
string.Underscore() | "SomeProperty" | "some_property" |
string.Kebaberize() | "SomeProperty" | "some-property" |
string.Titleize() | "some title" | "Some Title" |
string.Truncate() | "Long string..." | "Long stri…" |
string.Pluralize() | "child" | "children" |
string.Singularize() | "people" | "person" |
string.ToQuantity() | "file", 5 | "5 files" |
Enum.Humanize() | OrderStatus.InProgress | "In progress" |
"str".DehumanizeTo<T>() | "In progress" | OrderStatus.InProgress |
ByteSize.FromBytes().Humanize() | 1_500_000 | "1.43 MB" |
2.Hours() | — | TimeSpan.FromHours(2) |
7.Days() | — | TimeSpan.FromDays(7) |
Практичні завдання
Рівень 1 — Базовий
Завдання 1.1. Додайте до ShopApi endpoint GET /products/{id}/activity що повертає:
createdAgo— скільки часу тому створено товар (DateTime.Humanize())priceRange— якщо ціна менше 100 —"cheap", від 100 до 1000 —"medium", більше —"premium"(через власну логіку, але зHumanize()для форматування)sizeLabel— якщо є полеFileSizeBytes, відобразіть черезByteSize.Humanize()
Завдання 1.2. Реалізуйте endpoint GET /datetime-demo що повертає масив з 10 дат (від 5 секунд тому до 1 року тому), для кожної — Humanize() англійською, українською та польською мовами одночасно. Приклад виводу:
{
"original": "2024-03-01T12:00:00Z",
"en": "17 days ago",
"uk": "17 днів тому",
"pl": "17 dni temu"
}
Рівень 2 — Логіка
Завдання 2.1. Реалізуйте ActivityFeedService що повертає стрічку подій (список ActivityEvent: тип події через enum, дата, опис). Для кожної події: час через Humanize(), тип через Enum.Humanize() з [Description] атрибутами трьома мовами (через власні описи), розмір через ByteSize якщо застосовно.
Завдання 2.2. Напишіть HumanizedValidationResult — структуру що отримує колекцію помилок і рахує їх: "1 error found" або "3 errors found" через "error".ToQuantity(count). Для украномовного варіанту: "знайдено 3 помилки" (тут Pluralize() не підтримує uk — реалізуйте власну логіку відмінювання через словник форм слова).
Рівень 3 — Архітектура
Завдання 3.1. Реалізуйте власний IStringLocalizer-адаптер для Humanizer: клас HumanizerLocalizer що реалізує IStringLocalizer і для певних ключів делегує до Humanizer ("TimeAgo_{ticks}" → new DateTime(ticks).Humanize()). Зареєструйте його у DI поряд зі стандартним файловим локалайзером так, щоб можна було декорувати запити обома.
Підсумок
Humanizer — це бібліотека, яку встановлюєш один раз і використовуєш скрізь. Вона вирішує задачі, які формально прості, але вимагають окремого коду без неї: відмінювання, відносний час, кейс-конвертації, розмір файлів.
Відносний час
DateTime.Humanize() та TimeSpan.Humanize() — найпопулярніша функція. Підтримує 67 мов, включно з правильним відмінюванням в українській та польській.Числа та слова
ToWords(), ToOrdinalWords(), Ordinalize(), ToQuantity() — вирішують задачу відмінювань без власних умов. Pluralize() / Singularize() — для англійської.Рядкові перетворення
Pascalize(), Camelize(), Underscore(), Kebaberize(), Humanize() — повний набір для конвертації між стилями іменування.ByteSize та Enum
ByteSize.Humanize() для розмірів файлів, Enum.Humanize() з [Description] для читабельних статусів — щоденний інструмент у будь-якому API.Разом з IStringLocalizer для бізнес-текстів та CultureInfo для форматування чисел і дат — Humanizer закриває третій тип i18n-задач: читабельне представлення типів .NET. Ці три компоненти разом утворюють повноцінний i18n-стек для ASP.NET Core.
Інтернаціоналізація (i18n) у Minimal API: від A до Я
Повний курс з інтернаціоналізації ASP.NET Core Minimal API: IStringLocalizer, файли ресурсів .resx, визначення культури з URL / Accept-Language / Cookie, локалізація валідації, дат, чисел, валют. Практичний проєкт — мультимовний API магазину.
Огляд кешування: чотири рівні і коли що обирати
Огляд чотирьох механізмів кешування в ASP.NET Core: IMemoryCache, IDistributedCache, Response Cache, Output Cache. Порівняльна таблиця, стратегічна схема production-стека, правило вибору для кожного сценарію.