Humanizr/Humanizer · error · NotSupportedException
Custom formatter type
Error message
Custom formatter type '{GetType().FullName}' must explicitly implement IGrammaticalCaseTimeSpanFormatter to support grammatical-case-aware durations. What it means
Thrown by the explicit IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize implementation on DefaultFormatter when the formatter instance is a subclass defined outside the Humanizer assembly. The base implementation relies on internal generated case tables that only the built-in formatters can resolve, so any custom subclass that does not override this interface member is rejected. The fix is for the subclass to explicitly implement IGrammaticalCaseTimeSpanFormatter itself.
Solutions
- Have your custom formatter explicitly implement IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(TimeUnit, int, GrammaticalCase) to provide case-aware output for your locale.
- If your locale has no grammatical-case system, implement the interface to return the nominative form (delegating to the base TimeSpanHumanize(TimeUnit, int) overload).
- Avoid subclassing DefaultFormatter for case-aware locales; instead contribute the locale data so the generated tables include your culture.
Example fix
// before
public class MyFormatter : DefaultFormatter
{
public MyFormatter(CultureInfo c) : base(c) { }
// no IGrammaticalCaseTimeSpanFormatter override -> throws on case-aware calls
}
// after
public class MyFormatter : DefaultFormatter, IGrammaticalCaseTimeSpanFormatter
{
public MyFormatter(CultureInfo c) : base(c) { }
string IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(
TimeUnit timeUnit, int unit, GrammaticalCase grammaticalCase) =>
TimeSpanHumanize(timeUnit, unit); // fall back to nominative
} Defensive patterns
Strategy: type-guard
Validate before calling
// Before relying on the case-aware path with a custom formatter subclass,
// ensure it explicitly implements the interface:
if (formatter is not IGrammaticalCaseTimeSpanFormatter)
throw new InvalidOperationException("Formatter cannot handle grammatical case."); Type guard
static bool SupportsCaseAwareDurations(IFormatter f) =>
f.GetType().Assembly == typeof(DefaultFormatter).Assembly ||
f is IGrammaticalCaseTimeSpanFormatter; Try / catch
try { return ((IGrammaticalCaseTimeSpanFormatter)formatter).TimeSpanHumanize(unit, count, case); }
catch (NotSupportedException) { return formatter.TimeSpanHumanize(unit, count); } Prevention
- If you subclass DefaultFormatter, explicitly implement IGrammaticalCaseTimeSpanFormatter.
- Prefer composition or contributing locale data over subclassing for case-aware locales.
- Test custom formatters against case-aware duration calls, not just nominative ones.
When it happens
Trigger: Subclassing DefaultFormatter (e.g. public class MyFormatter : DefaultFormatter) in your own assembly and then invoking a case-aware duration path (one that calls IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize with a GrammaticalCase). The GetType().Assembly != typeof(DefaultFormatter).Assembly check detects the external subclass and throws NotSupportedException.
Common situations: Custom locale formatters that extend DefaultFormatter to tweak a few phrases; plugging a specialized formatter into Humanizer's registry for an unsupported dialect; upgrading Humanizer to a version that introduced case-aware formatting and discovering your subclass no longer works for Slavic/Baltic-style languages.
Related errors
- Culture ' ' does not apply grammatical case ' ' to duration…
- Culture ' ' does not support grammatical-case duration…
- Culture ' ' does not support grammatical case ' ' for…
- Culture ' ' has an applicable grammatical case system, but…
- Culture ' ' has no grammatical-case classification for…
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/e9007853c11ea89c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Humanizer/Localisation/Formatters/DefaultFormatter.cs:138
if (!toSymbols &&
(!phraseTable.TryGetTimeSpanPhrase(TimeUnit.Second, out var phrase) ||
phrase.Multiple is null))
{
throw new InvalidOperationException($"Missing generated fractional-second phrase for '{Culture.Name}'.");
}
return category;
}
string IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(
TimeUnit timeUnit,
int unit,
GrammaticalCase grammaticalCase)
{
if (GetType().Assembly != typeof(DefaultFormatter).Assembly)
{
throw new NotSupportedException(
$"Custom formatter type '{GetType().FullName}' must explicitly implement {nameof(IGrammaticalCaseTimeSpanFormatter)} to support grammatical-case-aware durations.");
}
if ((uint)grammaticalCase > (uint)GrammaticalCase.Causal)
{
throw new ArgumentOutOfRangeException(nameof(grammaticalCase), grammaticalCase, "Unsupported grammatical case.");
}
if ((uint)timeUnit > (uint)TimeUnit.Year)
{
throw new ArgumentOutOfRangeException(nameof(timeUnit), timeUnit, "Unsupported time unit.");
}
var table = LocaleDurationCaseTableCatalog.Resolve(Culture)
?? throw new NotSupportedException(
$"Culture '{Culture.Name}' has no grammatical-case classification for duration phrases.");
if (table.Classification == LocaleDurationCaseClassification.Unsupported)View on GitHub (pinned to ffc2b77c0f)