Humanizr/Humanizer · error · InvalidOperationException

Canonical list templates must use '{0}' and '{1}' as an infi

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.

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)

Solutions

  1. Rewrite the template as an infix: start with '{0}', end with '{1}', put the separator between them, e.g. 'pairTemplate: "{0}, {1}"'.
  2. If you only want a bare separator with no special final/serial behavior, use the legacy 'value' scalar instead of templates.
  3. 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

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


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