---
id: tag-resolution
title: 'Tag Resolution: the Term Stack'
description: 'How the Agility localizer resolves a tag through a stack of terms, from most specific to least specific, and what wins.'
sidebar_label: 'Tag Resolution'
sidebar_position: 3
tags: ['api', 'localization', 'concepts']
---


## A Tag Is a Stack, Not a Key

A localization tag is a **tic-separated stack of terms**, ordered from most
general to most specific:

```text
AttributeDefinition'Name'Story
└─ term 0 ──┘ └─ 1 ┘ └─ 2 ┘
```

Terms may not themselves contain a tic (`'`), because the tic is the separator.

When resolving a tag, the localizer looks for a template registered under the
whole tag, then progressively drops terms from the right until it finds one:

```text
Resolving  Plural'AssetType'Story
  try  Plural'AssetType'Story
  try  Plural'AssetType
  try  Plural
```

The first hit wins. This is why a single generic template can answer an
unbounded family of tags, and why adding one specific template quietly
overrides just that case.

The resolver trims the tag at its last separator, looks again, and repeats
until a template answers or nothing is left to trim.

### Worked Example

Given these templates:

```text
Plural={1}s
Plural'Story=Stories
```

| Tag             | Template found | Result    |
| --------------- | -------------- | --------- |
| `Plural'Scope`  | `Plural`       | `Scopes`  |
| `Plural'Story`  | `Plural'Story` | `Stories` |

The specific entry shadows the generic one for exactly one tag, and nothing
else changes.


A three-term case behaves the same way:

```text
Plural={1}s
```

resolves `Plural'AssetType'Story` by falling all the way back to `Plural`,
whose body then substitutes term 1 — `AssetType` — giving `AssetTypes`.


## Specificity Beats Position

Adding a longer tag never disturbs shorter ones, and the search always prefers
the longest registered prefix:

```text
Plural={1}s
Plural'Story=Stories
Report2=Report {Plural'1}/{Plural'2}
Report2'Member=Report: User by {2}
Scope=Project
```

| Tag                     | Result                    |
| ----------------------- | ------------------------- |
| `Report2'Scope'Story`   | `Report Projects/Stories` |
| `Report2'Member'Story`  | `Report: User by Story`   |
| `Report2'Member'Scope`  | `Report: User by Project` |


## Unmatched Tags Resolve to Themselves

If no prefix of the tag matches anywhere in the chain, the localizer returns the
tag itself, trimmed. It never errors, never returns null, and never omits the
key from a `loc-2.v1` response.

```json title="loc-2.v1?[bogus]"
{ "bogus": "bogus" }
```


This has two practical consequences:

- **You cannot detect a typo from the response.** A misspelled tag looks exactly
  like a tag that has no translation in the requested culture. If a value comes
  back identical to the tag you sent, check your spelling before filing a bug.
- **English-looking output is the designed default.** Agility's tags are already
  English words, so an unlocalized instance degrades to readable text rather
  than to `???`.

The empty tag is a special case that is pre-seeded to map to the empty string,
so `""` resolves to `""` rather than to itself.

## Results Are Trimmed, and Cached

The resolved value is `Trim()`ed before it is returned, which is what makes
optional trailing terms produce clean output:

```text
A=x {1}
```

| Tag   | Result |
| ----- | ------ |
| `A`   | `x`    |
| `A'B` | `x y`  |

With no term 1, `{1}` expands to nothing and the trailing space is trimmed away.


:::warning Trimming is edges only
`Trim()` removes leading and trailing whitespace from the finished string. A
missing substitution in the _middle_ of a template leaves a double space behind:
`x {1} z` with no term 1 yields `x  z`. Put optional terms at the end.
:::

Each localizer memoizes resolved tags. The cache is cleared whenever a template
is added or removed, so it is invisible to callers — but it does mean the first
request after a template change is fractionally slower.

## The Search Order Across the Fallback Chain

The trim-the-rightmost-term walk runs to completion **within one culture's
template map** before the fallback culture is consulted at all. The fallback is
then handed the _original, full_ tag and repeats the whole walk itself.

```text
Resolving Plural'AssetType'Story on fr-FR:

  fr-FR:  Plural'AssetType'Story -> Plural'AssetType -> Plural
  fr:     Plural'AssetType'Story -> Plural'AssetType -> Plural
  en:     Plural'AssetType'Story -> Plural'AssetType -> Plural
```

The two dimensions do **not** interleave. A generic `Plural` in `fr-FR` beats a
specific `Plural'AssetType'Story` in `en`. Specificity within a culture always
outranks specificity in a fallback culture.

This trips people up when adding translations: putting a very specific entry in
the default English set will not help if the target culture already has a broad
generic entry.


## Related Topics

- [Nested tag substitution](/apis/loc-2-v1/nested-tags) — what happens once a template is found
- [Culture fallback](/apis/loc-2-v1/culture-fallback) — how the fallback chain is built
- [Template sets and override order](/apis/loc-2-v1/template-sets) — how templates get into a culture's map
