{"record":{"id":"f7487c151a40d1d6","repo":"Humanizr/Humanizer","slug":"phrase-path-must-define-forms","errorCode":null,"errorMessage":"Phrase '{path}' must define forms.","messagePattern":"Phrase '(.+?)' must define forms\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs","lineNumber":307,"sourceCode":"                mapping,\n                path,\n                [\"forms\", \"default\", \"zero\", \"singular\", \"dual\", \"paucal\", \"plural\", \"many\", \"template\", \"countPlacement\", \"beforeCount\", \"afterCount\", .. allowedExtraKeys]);\n            var countPlacement = ParseCountPlacement(mapping, path);\n            var formPlaceholders = countPlacement == CountPlacement.None\n                ? [\"count\", \"prep\"]\n                : Array.Empty<string>();\n            var forms = ParseOptionalPhraseForms(\n                mapping,\n                path,\n                preserveDuplicateForms,\n                formPlaceholders);\n            var namedTemplate = mapping.TryGetValue(\"template\", out var templateValue)\n                ? ParseNamedTemplate(templateValue, $\"{path}.template\", [\"count\", \"unit\", \"prep\"])\n                : null;\n\n            if (forms is null)\n            {\n                throw new InvalidOperationException($\"Phrase '{path}' must define forms.\");\n            }\n\n            return new CountedPhrase(\n                preserveDuplicateForms ? forms : forms.CollapseDuplicates(),\n                countPlacement,\n                GetOptionalLiteral(mapping, \"beforeCount\", $\"{path}.beforeCount\", [\"prep\"]),\n                GetOptionalLiteral(mapping, \"afterCount\", $\"{path}.afterCount\", [\"prep\"]),\n                namedTemplate);\n        }\n\n        static (string? Single, string? WordsVariant) ParseSinglePhraseWithWordsVariant(SimpleYamlValue value, string path)\n        {\n            if (value is SimpleYamlScalar scalar)\n            {\n                return (ValidateLiteralText(path, scalar.Value), null);\n            }\n\n            var mapping = ExpectMapping(value, path);","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/Humanizr/Humanizer/blob/ffc2b77c0f30d2fb176875841424379319d0ae9b/src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs#L289-L325","documentation":"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).","triggerScenarios":"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.","commonSituations":"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'.","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."],"exampleFix":"# before\nduration:\n  hour:\n    multiple:\n      countPlacement: before\n# after\nduration:\n  hour:\n    multiple:\n      countPlacement: before\n      forms:\n        singular: hour\n        plural: hours","handlingStrategy":"validation","validationCode":"# Every 'multiple' mapping under duration or relativeDate must define forms.\n# Example check for duration units:\n#   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","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["humanizer","source-generator","locale","yaml","localization","phrases","counted-phrase","duration"],"backgroundTag":null,"analyzedSha":"ffc2b77c0f30d2fb176875841424379319d0ae9b","analyzedAt":"2026-08-13T21:42:34.584Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}