Humanizr/Humanizer · error · InvalidOperationException

Phrase '{path}' must define forms.

Error message

Phrase '{path}' must define forms.

What it means

Thrown by ParseCountedPhrase in the Humanizer source generator when a counted phrase mapping (used for 'multiple' inside duration/relativeDate phrases) defines no 'forms' and no inline form keys (default/singular/plural/...). Counted phrases must always carry form data because the count is interpolated into them. Surfaced as compiler diagnostic HSG003 (severity Error).

Source

Thrown at src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs:307

                mapping,
                path,
                ["forms", "default", "zero", "singular", "dual", "paucal", "plural", "many", "template", "countPlacement", "beforeCount", "afterCount", .. allowedExtraKeys]);
            var countPlacement = ParseCountPlacement(mapping, path);
            var formPlaceholders = countPlacement == CountPlacement.None
                ? ["count", "prep"]
                : Array.Empty<string>();
            var forms = ParseOptionalPhraseForms(
                mapping,
                path,
                preserveDuplicateForms,
                formPlaceholders);
            var namedTemplate = mapping.TryGetValue("template", out var templateValue)
                ? ParseNamedTemplate(templateValue, $"{path}.template", ["count", "unit", "prep"])
                : null;

            if (forms is null)
            {
                throw new InvalidOperationException($"Phrase '{path}' must define forms.");
            }

            return new CountedPhrase(
                preserveDuplicateForms ? forms : forms.CollapseDuplicates(),
                countPlacement,
                GetOptionalLiteral(mapping, "beforeCount", $"{path}.beforeCount", ["prep"]),
                GetOptionalLiteral(mapping, "afterCount", $"{path}.afterCount", ["prep"]),
                namedTemplate);
        }

        static (string? Single, string? WordsVariant) ParseSinglePhraseWithWordsVariant(SimpleYamlValue value, string path)
        {
            if (value is SimpleYamlScalar scalar)
            {
                return (ValidateLiteralText(path, scalar.Value), null);
            }

            var mapping = ExpectMapping(value, path);

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Add a 'forms' mapping (or direct singular/plural/etc. keys) to the offending multiple block.
  2. If a single uninflected word suffices, make the multiple value a scalar string.
  3. Verify that countPlacement is set only when forms are present, since forms are mandatory regardless.
  4. Rebuild and confirm HSG003 no longer fires for that locale.

Example fix

# before
duration:
  hour:
    multiple:
      countPlacement: before
# after
duration:
  hour:
    multiple:
      countPlacement: before
      forms:
        singular: hour
        plural: hours
Defensive patterns

Strategy: validation

Validate before calling

# Every 'multiple' mapping under duration or relativeDate must define forms.
# Example check for duration units:
#   yq -o=json '.phrases.duration | to_entries | map(.value.multiple // empty) | map(select(. != null and (.forms // .singular // .plural // .default) == null))' src/Humanizer/Locales/<code>.yml

Prevention

When it happens

Trigger: Authoring phrases.duration.<unit>.multiple (or relativeDate.<scope>.<unit>.multiple) as a mapping that sets only countPlacement/beforeCount/afterCount/template-placeholder fields but provides no forms and no direct form keys.

Common situations: Adding a 'multiple' block to customize count placement and forgetting the actual word forms; setting beforeCount/afterCount around an empty phrase; relying on a template placeholder name that is not 'forms'.

Related errors


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