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
- Add a 'forms' mapping (or direct singular/plural/etc. keys) to the offending multiple block.
- If a single uninflected word suffices, make the multiple value a scalar string.
- Verify that countPlacement is set only when forms are present, since forms are mandatory regardless.
- 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
- Remember 'multiple' is a counted phrase: forms are mandatory even when you only want countPlacement.
- Add forms first, then customize countPlacement/beforeCount/afterCount.
- Build incrementally while editing locale YAML.
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
- Phrase '{path}' must define 'numeric', 'text', or 'words'.
- Phrase '{path}' must define a scalar string or an explicit '
- Phrase '{path}' must define forms, 'symbol', or 'template'.
- Phrase forms '{path}' must define at least one form.
- Phrase '{path}.countPlacement' uses unsupported count placem
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/f7487c151a40d1d6.
Report an issue: GitHub.