Humanizr/Humanizer · error · InvalidOperationException

Locale '{localeCode}.surfaces.list' must define canonical li

Error message

Locale '{localeCode}.surfaces.list' must define canonical list templates or legacy 'value'.

What it means

Thrown by NormalizeListSurface when surfaces.list declares an engine and has no legacy 'value', but is missing the required 'pairTemplate'. The canonical list engine needs at least pairTemplate to build the join logic. The only exception is engine: 'oxford' with no templates at all (which uses built-in defaults). Any other engine without pairTemplate fails.

Source

Thrown at src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs:590

        {
            if (mapping.TryGetValue("value", out _))
            {
                return mapping;
            }

            var engine = mapping.GetScalar("engine")
                ?? throw new InvalidOperationException($"Locale '{localeCode}.surfaces.list' must define 'engine'.");
            var pairTemplate = mapping.GetScalar("pairTemplate");
            var finalTemplate = mapping.GetScalar("finalTemplate");
            var serialTemplate = mapping.GetScalar("serialTemplate");
            if (engine == "oxford" && pairTemplate is null && finalTemplate is null)
            {
                return mapping;
            }

            if (pairTemplate is null)
            {
                throw new InvalidOperationException(
                    $"Locale '{localeCode}.surfaces.list' must define canonical list templates or legacy 'value'.");
            }

            finalTemplate ??= pairTemplate;

            var values = ImmutableDictionary.CreateBuilder<string, SimpleYamlValue>(StringComparer.Ordinal);
            var oxfordComma = ReadBoolean(mapping, "oxfordComma");
            var cliticizesFinal = ReadBoolean(mapping, "cliticizesFinal");
            values["engine"] = new SimpleYamlScalar(
                oxfordComma || engine == "oxford"
                    ? "oxford"
                    : cliticizesFinal
                        ? "clitic"
                        : engine == "delimited" || pairTemplate == finalTemplate && pairTemplate == serialTemplate
                            ? "delimited"
                            : "conjunction",
                isQuoted: true);

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Add a 'pairTemplate' scalar using {0} and {1} placeholders, e.g. 'pairTemplate: "{0}, {1}"'.
  2. If you intended the oxford default templates, set 'engine: oxford' and remove all template keys so the built-in oxford defaults apply.
  3. Switch to the legacy 'value' form (a bare separator scalar) if you only need simple delimited output.

Example fix

# before
surfaces:
  list:
    engine: conjunction
# after
surfaces:
  list:
    engine: conjunction
    pairTemplate: "{0}, {1}"
Defensive patterns

Strategy: validation

Validate before calling

// Verify pairTemplate is present when engine is not bare-oxford
static bool ListTemplatesAreComplete(SimpleYamlMapping listMap)
{
    if (listMap.TryGetValue("value", out _)) return true;
    var engine = listMap.GetScalar("engine");
    if (engine == "oxford" && listMap.GetScalar("pairTemplate") is null && listMap.GetScalar("finalTemplate") is null)
        return true;
    return listMap.GetScalar("pairTemplate") is not null;
}

Prevention

When it happens

Trigger: NormalizeListSurface checks: if value is absent and engine is present and not (oxford with no templates), then pairTemplate must exist. For example 'surfaces:\n list:\n engine: conjunction' with no pairTemplate triggers it.

Common situations: A contributor sets engine to 'conjunction' or 'delimited' but only provides finalTemplate or serialTemplate, forgetting pairTemplate. Or they partially fill in templates after copying an oxford block (which doesn't require them).

Related errors


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