---
id: culture-fallback
title: 'Culture Fallback'
description: 'How Accept-Language maps to a localizer, how the specific/neutral/default fallback chain works, and what happens when a culture is partially or wholly unmatched.'
sidebar_label: 'Culture Fallback'
sidebar_position: 5
tags: ['api', 'localization', 'i18n', 'concepts']
---


## From Header to Localizer

```text
Accept-Language header
   └─ first entry only
        └─ CultureInfo.CreateSpecificCulture(...)
             └─ localizer for that culture
                  └─ fallback chain
```

Two things about the first step surprise people:

- **Only the first language in the header is used.** Agility does not evaluate
  `q` values or try the second-choice language. `Accept-Language: de,fr;q=0.9`
  is treated as `de`.
- **Neutral cultures are expanded.** `CreateSpecificCulture("fr")` yields
  `fr-FR`. You cannot request a neutral culture over HTTP; you always land on a
  specific one, which then falls back through the neutral one.

An unparseable value, or a missing header, produces the invariant culture, which
resolves to the default localizer — English.


## The Chain

Each localizer has at most one fallback localizer. `LocalizationManager` wires
them up as:

```text
specific culture  ->  neutral (parent) culture  ->  default culture
        fr-FR              fr                          en
```

Agility's default culture is `en`.


## Per-Tag Fallback

Fallback is evaluated **per tag**, not per culture. A culture that defines only
a handful of terms inherits everything else.

Using the library's own fixtures, where `en.Base` defines all three tags and
`fr.Base` / `fr-FR.Base` define progressively more specific ones:

| Requested culture | `TestDefault`     | `TestNeutral`     | `TestSpecific`             |
| ----------------- | ----------------- | ----------------- | -------------------------- |
| `en`              | `Default English` | `Neutral English` | `Specific English`         |
| `en-US`           | `Default English` | `Neutral English` | `Specific American English` |
| `fr`              | `Default English` | `Neutral French`  | `Specific French`          |
| `fr-FR`           | `Default English` | `Neutral French`  | `Specific France French`   |
| `fr-CH`           | `Default English` | `Neutral French`  | `Specific French`          |
| `de`              | `Default English` | `Neutral English` | `Specific English`         |
| `de-AT`           | `Default English` | `Neutral English` | `Specific English`         |


Read the `fr-CH` and `de-AT` rows carefully. `fr-CH` has no templates of its
own, so it behaves exactly like `fr`. `de-AT` has neither a specific nor a
neutral set, so it behaves exactly like `en`. **A wholly unmatched culture is
not an error and produces no marker in the response** — it silently returns the
default culture's text.


:::note Instance reuse is a library detail
Those four tests assert instance _identity_ — `de` returns the very same
`Localizer` object as `en`. In Agility that optimisation does not fire, because
one of Core's template providers contributes a set for every culture, so each
requested culture gets its own localizer object chained to its parent. The
observable per-tag behaviour in the table above is unchanged; only the object
graph differs.

:::

## The Behaviour Nobody Expects

**A template found in a fallback culture is expanded against the culture you
asked for, not the culture it was found in.**

Nested tags in the template body are resolved by the localizer that received the
request. So a template can live in English while every word it splices in comes
from French.

Given:

```text title="en.Base.txt"
XYZ=ABC {DEF} GHI
DEF=JKL
```

```text title="fr-FR.Base.txt"
DEF=PQR
```

Requesting `XYZ` in `fr-FR` returns:

```text
ABC PQR GHI
```

The `XYZ` template exists only in English. Its `{DEF}` placeholder is
nonetheless resolved by the `fr-FR` localizer, which has its own `DEF`.


This is the mechanism that lets Agility ship one English sentence structure and
have every embedded noun localize correctly. It is also the mechanism that
produces mixed-language output when a culture translates a noun but not the
sentence around it — which is working as designed, not a bug.

The same rule applies to instance-wide overrides:

| Tag   | `en` | `fr` | `fr-FR` |
| ----- | ---- | ---- | ------- |
| `DEF` | `fed` (override) | `fed` (inherited) | `PQR` (culture wins) |
| `XYZ` | `ABC fed GHI` | `ABC fed GHI` | `ABC PQR GHI` |


## Specificity Does Not Cross Cultures

Worth repeating from [Tag resolution](/apis/loc-2-v1/tag-resolution#the-search-order-across-the-fallback-chain),
because it is the most common cause of "my translation is being ignored":

The term-stack walk completes **entirely within one culture** before the next
culture is consulted. A generic `Plural` in `fr-FR` beats a specific
`Plural'Story` in `en`.

If a French user is seeing a French generic plural where you expected an English
irregular one, that is why — and the fix is to add the specific entry to the
French set, not the English one.

## Checking What a Culture Actually Returns

```bash title="Compare two cultures in two requests"
curl "https://www.example.com/VersionOne/loc-2.v1?[Story,Plural'Story,Scope]" \
  -H "Accept-Language: en-US"

curl "https://www.example.com/VersionOne/loc-2.v1?[Story,Plural'Story,Scope]" \
  -H "Accept-Language: fr-FR"
```

Because a wholly unmatched culture is indistinguishable from a matched one in
the response body, diffing two cultures like this is the practical way to tell
whether a translation set is installed at all. If every value is identical,
either the culture has no templates or the tags have no translations — the API
cannot tell you which. Pair this with
[`noloc`](/apis/loc-2-v1/noloc) to separate "not translated" from "not localized
at all".

## Related Topics

- [Requests and responses](/apis/loc-2-v1/requests-and-responses#choosing-a-culture) — the `Accept-Language` contract
- [Template sets and override order](/apis/loc-2-v1/template-sets) — how a culture's templates are assembled
- [Debugging with `noloc`](/apis/loc-2-v1/noloc)
