Humanizr/Humanizer · critical · InvalidOperationException
Locale inheritance cycle detected at '{localeCode}'.
Error message
Locale inheritance cycle detected at '{localeCode}'. What it means
During locale resolution the source generator walks each locale's 'inherits' chain recursively, tracking the current path in a HashSet. If it re-enters a locale already on the resolution stack, a circular inheritance graph exists and the generator aborts with a hard build failure rather than producing infinite recursion or incorrect merged data.
Source
Thrown at src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs:457
static ResolvedLocaleDefinition ResolveLocale(
string localeCode,
Dictionary<string, LocaleDefinition> parsedLocales,
HashSet<string> resolving,
ImmutableArray<ResolvedLocaleDefinition>.Builder cache,
ImmutableArray<Diagnostic>.Builder diagnostics)
{
if (cache.FirstOrDefault(cached => cached.LocaleCode == localeCode) is { } cached)
return cached;
if (!parsedLocales.TryGetValue(localeCode, out var locale))
{
throw new InvalidOperationException($"Locale '{localeCode}' is not defined.");
}
if (!resolving.Add(localeCode))
{
throw new InvalidOperationException($"Locale inheritance cycle detected at '{localeCode}'.");
}
ResolvedLocaleDefinition inherited;
if (string.IsNullOrWhiteSpace(locale.Inherits))
{
inherited = ResolvedLocaleDefinition.Empty(localeCode);
}
else
{
var inheritedLocale = locale.Inherits!;
if (!parsedLocales.ContainsKey(inheritedLocale))
{
diagnostics.Add(Diagnostic.Create(
HumanizerSourceGenerator.Diagnostics.InvalidLocaleDefinition,
Location.None,
localeCode,
$"Inherited locale '{inheritedLocale}' is not defined."));
inherited = ResolvedLocaleDefinition.Empty(localeCode);View on GitHub (pinned to ffc2b77c0f)
Solutions
- Trace the inherits chain starting from the locale code named in the message until you find the node that points back into the chain.
- Break the cycle by removing or correcting the 'inherits:' field on the offending locale so the graph becomes a DAG rooted at a language-only locale (e.g. 'en', 'fr', 'de').
- Run a quick grep of all 'inherits:' values to verify no locale is its own ancestor.
Example fix
# before — cycle: # fr-CA.yml inherits: fr-CH # fr-CH.yml inherits: fr-CA # after — DAG: # fr-CA.yml inherits: fr # fr-CH.yml inherits: fr
Defensive patterns
Strategy: validation
Validate before calling
# Before building, verify no inherits cycle exists in all locale YAML files.
# Run as a pre-commit or CI check (PowerShell or bash + yq):
#
# Collect every locale file and build the inherits graph, then detect cycles.
import pathlib, re
locales_dir = pathlib.Path('src/Humanizer/Locales')
graph = {}
for f in locales_dir.rglob('*.yml'):
text = f.read_text()
code = f.stem # or parse 'locale:' key
m = re.search(r'^inherits:\s*(\S+)', text, re.MULTILINE)
graph[code] = m.group(1) if m else None
def has_cycle(node, graph, seen):
if node in seen:
return True
if node is None or node not in graph:
return False
seen.add(node)
return has_cycle(graph[node], graph, seen)
for code in graph:
if has_cycle(code, graph, set()):
print(f'CYCLE detected involving {code}') Prevention
- Treat locale inheritance as a DAG rooted at language-only locales (en, fr, de) and never point a root back to a variant.
- Add a CI step that builds the inherits graph and rejects cycles before the source generator runs.
- When renaming or splitting a locale, grep all 'inherits:' references to the old code and update them.
When it happens
Trigger: Two or more locale YAML files form a cycle via their 'inherits:' field. For example 'sr-Latn.yml' declares 'inherits: sr' while 'sr.yml' declares 'inherits: sr-Latn'. A self-referential 'inherits: <own-code>' also triggers it.
Common situations: Refactoring or splitting locale files and rewiring inherits pointers; renaming a locale tag without updating dependents; accidentally setting a parent locale to inherit from a child variant.
Related errors
- '{path}' contains a sameRenderedAs cycle at '{node}'.
- Locale '{localeCode}' is not defined.
- Locale '{localeCode}' must author durationCases instead of i
- Locale '{localeCode}.{path}' defines duplicate numeric minut
- Locale '{localeCode}.{path}.{key}' must be a string.
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/4116d6860e0e7ec7.
Report an issue: GitHub.