Humanizr/Humanizer · error · InvalidOperationException

Locale '{document.LocaleCode}.surfaces.{surface.Key}' must b

Error message

Locale '{document.LocaleCode}.surfaces.{surface.Key}' must be a mapping.

What it means

ToLocaleDefinition re-checks that each surface value is a mapping before dispatching on its name (CanonicalLocaleAuthoring.cs:139-145). The Parse path already enforces this, so reaching this throw means a CanonicalLocaleDocument was constructed or mutated outside Parse (e.g. legacy migration, a test fixture, or code that built the document by hand).

Source

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

            return new CanonicalLocaleDocument(
                localeCode,
                variantOf,
                surfaces,
                NormalizeCanonicalText(fileText));
        }

        internal static string Emit(CanonicalLocaleDocument document) => document.CanonicalText;

        internal static LocaleDefinition ToLocaleDefinition(CanonicalLocaleDocument document)
        {
            var features = ImmutableDictionary.CreateBuilder<string, SimpleYamlValue>(StringComparer.Ordinal);

            foreach (var surface in document.Surfaces.Values)
            {
                if (surface.Value is not SimpleYamlMapping surfaceMapping)
                {
                    throw new InvalidOperationException(
                        $"Locale '{document.LocaleCode}.surfaces.{surface.Key}' must be a mapping.");
                }

                switch (surface.Key)
                {
                    case "list":
                        features["collectionFormatter"] = NormalizeListSurface(document.LocaleCode, surfaceMapping);
                        break;

                    case "formatter":
                        features["formatter"] = surfaceMapping;
                        var grammar = ImmutableDictionary.CreateBuilder<string, SimpleYamlValue>(StringComparer.Ordinal);
                        foreach (var propertyName in FormatterGrammarPropertyNames.Where(surfaceMapping.Values.ContainsKey))
                        {
                            grammar[propertyName] = surfaceMapping.Values[propertyName];
                        }

                        if (grammar.Count > 0)

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Always obtain documents via CanonicalLocaleAuthoring.Parse so the surface-shape invariant holds.
  2. If constructing programmatically, ensure every value in the surfaces mapping is a SimpleYamlMapping.
  3. Reproduce via Parse to surface the earlier, more specific error and fix it there.

Example fix

// before (test/migration builds a document with a scalar surface)
var doc = new CanonicalLocaleDocument("en", null, surfacesWithScalarClock, text);
ToLocaleDefinition(doc); // throws
// after
var doc = CanonicalLocaleAuthoring.Parse("en", canonicalYaml); // surfaces.clock is a mapping
Defensive patterns

Strategy: type-guard

Validate before calling

// Before calling ToLocaleDefinition, ensure every surface is a mapping
static bool AllSurfacesAreMappings(CanonicalLocaleDocument d)
    => d.Surfaces.Values.All(kv => kv.Value is SimpleYamlMapping);

Type guard

static bool IsValidLocaleDocument(CanonicalLocaleDocument d) =>
    d.Surfaces.Values.All(kv => kv.Value is SimpleYamlMapping);

Prevention

When it happens

Trigger: A code path builds a CanonicalLocaleDocument directly with a non-mapping surface value and passes it to ToLocaleDefinition; or the migration helper emits a malformed surface.

Common situations: Writing a unit test that constructs CanonicalLocaleDocument manually; extending the migration tooling and producing a sequence/scalar surface.

Related errors


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