Humanizr/Humanizer · error · InvalidOperationException
Canonical list templates must use
Error message
Canonical list templates must use '{0}' and '{1}' as an infix template. What it means
Thrown by ExtractTemplateSeparator when a canonical list template (pairTemplate, finalTemplate, or serialTemplate) does not start with '{0}' and end with '{1}'. The generator extracts the infix separator between these two placeholders to produce the legacy 'value' string. Templates that don't follow this exact infix shape cannot be normalized.
Solutions
- Rewrite the template as an infix: start with '{0}', end with '{1}', put the separator between them, e.g. 'pairTemplate: "{0}, {1}"'.
- If you only want a bare separator with no special final/serial behavior, use the legacy 'value' scalar instead of templates.
- Verify that finalTemplate and serialTemplate (if present) also follow the same {0}...{1} shape.
Example fix
# before
surfaces:
list:
engine: conjunction
pairTemplate: ", "
# after
surfaces:
list:
engine: conjunction
pairTemplate: "{0}, {1}" Defensive patterns
Strategy: validation
Validate before calling
// Ensure list templates use {0}...{1} infix shape
static bool TemplateIsInfix(string template) =>
template.StartsWith("{0}", StringComparison.Ordinal) &&
template.EndsWith("{1}", StringComparison.Ordinal) &&
template.Length >= "{0}{1}".Length; Prevention
- All list templates must start with {0} and end with {1} with the separator between them.
- Use 'value' (bare separator) instead of templates for simple delimited output.
- Test template strings with a quick regex: ^\\{0\}.*\\{1\}$
When it happens
Trigger: NormalizeListSurface calls ExtractTemplateSeparator on finalTemplate (which defaults to pairTemplate if finalTemplate is absent). If the template string lacks the {0}...{1} wrapper — e.g. 'pairTemplate: ", "' or 'pairTemplate: "{0}{1}"' — StartsWith/EndsWith fails and the throw expression fires.
Common situations: A contributor writes a plain separator string (e.g. ', ') instead of a full template, or puts the separator outside the placeholders. Also occurs when copy-pasting a template from documentation that omits the placeholders.
Related errors
- Locale ' .surfaces.list' must define canonical list…
- Locale ' .surfaces.list' must define 'engine'.
- Phrase ' ' can only use placeholders in explicit 'template'…
- Phrase ' ' must define a scalar string or an explicit…
- Template ' ' can only use placeholders: .
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/255ba54cc131c998.
Report an issue: GitHub.
Appendix: source
Thrown at src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs:629
values["value"] = new SimpleYamlScalar(ExtractTemplateSeparator(finalTemplate), isQuoted: true);
}
return new SimpleYamlMapping(values.ToImmutable());
}
static bool ReadBoolean(SimpleYamlMapping mapping, string key) =>
mapping.TryGetValue(key, out var value) &&
value is SimpleYamlScalar scalar &&
bool.TryParse(scalar.Value, out var result) &&
result;
static string ExtractTemplateSeparator(string template)
{
const string start = "{0}";
const string end = "{1}";
return !template.StartsWith(start, StringComparison.Ordinal) ||
!template.EndsWith(end, StringComparison.Ordinal)
? throw new InvalidOperationException("Canonical list templates must use '{0}' and '{1}' as an infix template.")
: template.Substring(start.Length, template.Length - start.Length - end.Length).Trim();
}
}
public static class LegacyLocaleMigration
{
static readonly string[] LegacyTopLevelNames =
[
"inherits",
"collectionFormatter",
"dateOnlyToOrdinalWords",
"dateToOrdinalWords",
"durationCases",
"formatter",
"grammar",
"headings",
"numberToWords",
"ordinalizer",View on GitHub (pinned to ffc2b77c0f)