One content tree, culture variants on the nodes, and a domain per culture on the site root. That is Umbraco's documented model: one node holds every language version, and the domain the request matches decides which one renders. Build it in this order, since each step depends on the one before.
| # | Step | Reference | Ships |
|---|---|---|---|
| 1 | Languages: default, mandatory, fallback | languages-and-variants.md | assets/multilingual-package.xml |
| 2 | Vary Document Types, properties and blocks by culture | languages-and-variants.md | — |
| 3 | Culture and Hostnames: a domain per culture | languages-and-variants.md | — |
| 4 | Render variant values, fallback, dictionary, culture outside a request | rendering-variants.md | — |
| 5 | Language switcher and hreflang | language-switcher-and-seo.md | languageSwitcher.cshtml, hreflangLinks.cshtml |
| — | Only if a culture is missing from the Languages dropdown | additional-cultures.md | AllowAdditionalCulturesComposer.cs |
How to decide
- Default to one tree with culture variants. Choose a root per market only when markets differ in structure, not just in wording; see the note in languages-and-variants.md.
- For backoffice steps (1–3), prefer the Umbraco Developer MCP. If it's unavailable, walk the user through the backoffice one step at a time; the order is in languages-and-variants.md. Don't skip a step because the MCP is missing.
- Headless / Content Delivery API front ends are out of scope for now. This skill covers Razor sites.
Version compatibility
Targets Umbraco 17+. Every API used here (ILanguageService, IDomainService.UpdateDomainsAsync,
IIsoCodeValidator, ICultureService) exists on 17, with one exception: language fallback for
blocks (Value<BlockListModel>(..., fallback: Fallback.ToLanguage) showing a block not exposed in
the current language) needs 17.4+. On 17.0–17.3 the argument is accepted but unexposed blocks
stay hidden, with no error, so check the site's version before relying on it. Umbraco 13 and earlier
have different language/domain APIs; don't apply this skill there.
Best practices
- Never lowercase a culture you pass to Umbraco APIs or emit in markup. Keep ISO casing
(
da-DK) in code and config, and compare withStringComparison.OrdinalIgnoreCase. The one exception is Examine field names, whose culture suffixes are stored lowercase (__Published_da-dk,title_da-dk): build those withculture.ToLowerInvariant(), or a query on__Published_da-DKsilently matches nothing. - Don't hardcode culture lists. Read them from
ILanguageServiceorIPublishedContent.Cultures, so adding a language needs no code change. - Set a fallback per language and decide mandatory deliberately. A mandatory language blocks publishing every other culture of a node until it has content.
- Translate blocks with block level variance, not a varying block property. Three settings, all three stated: the Document Type varies by culture, the Block List/Grid property is invariant (shared), and its element types vary. Every language then shares the same blocks and order. A varying block property gives each language its own list, and the lists drift.
- Vary only what differs. Shared values (SKUs, dates, images with no text) belong on invariant properties, or editors enter them once per language and they drift apart.
Value<string>returns"", notnull, for a blank value. A?? fallbacknever fires; useFallback.ToLanguageorstring.IsNullOrWhiteSpace.Umbraco:CMS:Global:DefaultUILanguageis the backoffice UI language. The site's default content language is set on the language itself (Settings → Languages → Default language). Changing that default is a separate decision from adding a language: the default is what renders when no domain matches and whatFallback.ToDefaultLanguagereads, so when a launch request implies making the new language the default, ask whether that is really intended rather than switching it.- Every published culture needs a reachable URL. If a culture has no domain, its pages can't be routed in that culture, and the switcher and hreflang tags have nothing to link to.
- The runtime gate tests the skill's assets, not your copy of them. When handing over, say the partials or composer were copied from this skill and whether you built the project. Don't present the gate as your verification ("validated against Umbraco 17.5") unless you ran it.
Validation
Assertions live in evals/evals.json; run them with umbraco-skill-evaluator.
evals/README.md has the project fixture, the run-prompt rules, and the Haiku and
Sonnet results (with and without the skill).
The runtime gate covers both gated parts:
examples/culture-variants/on the blank host (languages, domains, fallback, dictionary, switcher, hreflang, block level variance);examples/additional-cultures/on the Clean host (the composer).