# Languages
One config, N languages. No duplicate files.
## Declaring locales
```yaml
# site.yaml
locales: [fr, en]
```
The first entry is the default: it is the fallback for missing translations
and for visitors whose browser language is not served. Locale codes appear in
URLs (`/fr/`, `/en/`) and match browser languages by base tag (`fr-CA`
visitors get `fr`).
## How a visitor gets a language
1. Their saved choice. Only the header switcher sets the one-year cookie
(its links carry `?choose`); a plain visit never does, so a visitor who
changes their browser language is followed until they explicitly pick
one.
2. Otherwise the browser's `Accept-Language`.
3. Otherwise the default locale.
`/` redirects accordingly, and the sitemap lists every locale of every
page, so search engines index each language separately.
## Translated fields
Every field typed _text_ in the [reference](../reference.md) accepts either
form: service names, descriptions and details, the tagline, the welcome
note, link and footer labels, hosted-page titles and bodies, category
names, UI strings.
```yaml
name: PDF toolbox # every locale
name: { fr: Boîte à outils PDF, en: PDF toolbox } # per locale
```
A plain string applies to every locale. A per-locale map resolves to the
visitor's locale, then the default locale, then any value rather than
nothing.
That fallback is what makes a half-finished map hard to see: the page reads
fine, in the wrong language. `cairn -check` lists every field of the kind above
that answers for some of your locales and not the rest, the site title and the
link labels included, and names the locales it misses.
The page says so too, to the browser. Text that came from another language
carries a `lang` of its own, and a `dir` when the direction differs from the
page's:
```html
Everything in one place
محرر نصوص
```
That is what stops a screen reader reading English with a French voice, and it
is what keeps an Arabic service name from coming out with its punctuation at
the wrong end of the line inside a French page. A fully translated site emits
none of it: the marker exists only where the wording really did come from
somewhere else. A regional variant is not somewhere else, so `pt` wording on a
`pt-BR` page stays unmarked.
Four places cannot carry it, and no trick would change that: the ``, the
meta and og descriptions, and the status pill's `aria-label` are attribute or
chrome text, with no element to hang an attribute on. A fallback there is
silent. The visible pill next to the label is marked, which is the half that
can be.
## UI strings
The interface text ships built in for **English, French, German, Spanish,
Italian, Dutch, Portuguese and Swedish**; a regional code finds its base language
(`pt-BR` uses the `pt` table). Any other locale falls back to English until
you override it: `strings` in `site.yaml` accepts any locale:
```yaml
# site.yaml: cairn in Polish
locales: [pl]
strings:
nav.skip: Przejdź do treści
search.label: Szukaj
search.placeholder: Szukaj narzędzia…
search.empty: Brak wyników. Spróbuj innego słowa.
cat.other: Inne
card.more: Dowiedz się więcej
detail.open: Otwórz
detail.back: Wstecz
```
Missing your language? Adding it to cairn itself is a small, welcome pull
request: see [Add your language](../../CONTRIBUTING.md#add-your-language).
Until then, `cairn -check` says which locale is in that position rather than
leaving you to spot English menus on a Polish page:
```console
warning: locale "pl" has no built-in interface: its pages carry that lang and an English menu, buttons and search messages (cairn ships de, en, es, fr, it, nl, pt, sv; a strings: block supplies the rest)
```
A word on quality: English, French and Swedish are written by people who speak
them. The other five are careful but unreviewed, and interface words are
exactly where a slightly-off translation grates. If one of them is your
language and a string reads wrong, correcting it is a one-line pull request and
genuinely welcome: Swedish arrived that way. Five Swedish strings are not yet
reviewed by a speaker: the folded welcome note and its dismiss button
(`about.summary`, `about.dismiss`), the search placeholder
(`search.placeholder`) and the screenshot strip (`shot.scroll`, `shot.full`).
## Right-to-left languages
Set a right-to-left locale and the page turns around on its own: cairn writes
`dir="rtl"` on `` for Arabic, Hebrew, Persian, Urdu and the others, and
the stylesheet is written with logical properties, so the whole layout mirrors
rather than being patched side by side.
```yaml
# site.yaml
locales: [ar, en]
```
The interface text is a separate matter: none of the eight built-in languages
is right-to-left, so the chrome falls back to English until you translate it
with `strings` above. The direction works either way; the words are yours.
| Key | English default | Where |
| -------------------- | ------------------------------------------------------------ | --------------------------------------------------------- |
| `nav.skip` | Skip to content | keyboard skip link |
| `nav.languages` | Language | switcher label (a11y) |
| `nav.toc` | Categories | category trail label (a11y) |
| `nav.links` | Links | header nav row label (a11y) |
| `nav.menu` | Menu | the word on the burger button, on a phone |
| `nav.theme` | Theme | theme toggle label (a11y) |
| `nav.top` | Back to top | back-to-top arrow label (a11y) |
| `about.dismiss` | Dismiss | welcome note close button |
| `about.summary` | About this site | the welcome note's folded row, on a phone |
| `foot.powered` | powered by | footer credit, before "cairn" |
| `search.label` | Search | search input label (a11y) |
| `search.placeholder` | Search for a tool… | search input |
| `search.empty` | No results. Try another word. | empty search state |
| `search.one` | 1 result | screen-reader count, singular |
| `search.many` | %d results | screen-reader count, plural (`%d` is the number) |
| `cat.other` | Other | implicit category |
| `card.more` | Learn more | names the detail-page glyph for a screen reader |
| `host.self` | Self-hosted | hosting flag on a card, `selfhosted: true` |
| `host.external` | External | hosting flag on a card, `selfhosted: false` |
| `state.soon` | Coming soon | badge on a service not open yet |
| `state.retired` | No longer available | badge on a service that is gone |
| `state.beta` | Beta | badge on a service with rough edges |
| `state.deprecated` | Deprecated | badge on a service on its way out |
| `state.new` | New | badge you add and remove yourself |
| `detail.open` | Open | detail page button |
| `detail.back` | Back | detail page back link |
| `shot.scroll` | Screenshot, scroll sideways | names a screenshot strip, then its number (a11y) |
| `shot.full` | See the full screenshot | link under each screenshot to the whole image |
| `status.up` | Online | status pill label |
| `status.down` | Offline | status pill label |
| `status.degraded` | Degraded | pill label: works, but not well |
| `status.maintenance` | Maintenance | pill label: off on purpose |
| `status.unknown` | Unknown | pill label before the monitor answers |
| `status.link` | view status | status pill link hint (a11y) |
| `leave.title` | You are leaving this directory | heading of the [leaving dialog](site.md#leaving-the-site) |
| `leave.body` | This address belongs to someone else. Read it before you go. | its sentence |
| `leave.copy` | Copy the address | its copy button |
| `leave.copied` | Copied | what that button says once it has |
| `leave.go` | Continue | its link to the destination |
| `leave.stay` | Stay here | its cancel button |
| `notfound.title` | Page not found | heading of the page for an address that leads nowhere |
| `notfound.body` | Nothing lives at this address. The link may be out of date. | its sentence |
A partial override only applies to the locales it names; the built-ins fill
the rest. So `nav.menu: { fr: Sommaire }` on a `[fr, en]` site gives the French
pages your word and the English pages cairn's, and neither page shows which of
the two it is using. `cairn -check` names the key and the locales it leaves
out.
Next: [Docker Compose](../deployment/docker-compose.md)