Humanizr/Humanizer · error · NotSupportedException

Custom formatter type '{GetType().FullName}' must explicitly

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.

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)

Solutions

  1. Have your custom formatter explicitly implement IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize(TimeUnit, int, GrammaticalCase) to provide case-aware output for your locale.
  2. If your locale has no grammatical-case system, implement the interface to return the nominative form (delegating to the base TimeSpanHumanize(TimeUnit, int) overload).
  3. 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

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


AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13). Data as JSON: /api/errors/e9007853c11ea89c. Report an issue: GitHub.