---
title: A vzk site in several languages
description: Use this guide when a vzk site speaks more than one language.
sidebar:
  order: 10
---

Use this guide when a vzk site speaks more than one language. The site is
written once, in its first language, in its `.vzk` files, and every other
language is a folder of catalogs that translate those files' words.

## Declare languages

Call `set_site_languages` with the default language and the additional ones.
The default is the site's first language: the one its files are written in.
This is a site setting, not a draft: it is live when the call returns,
whether or not a catalog exists yet. A text with no translation is drawn as
the file has it, so a site can be translated one text at a time.

`language` in `site.json` says what the language of a page goes by:

| `language` | An address | A link to a page |
| --- | --- | --- |
| nothing, or `"both"` | `/uk/journal` is the journal in `uk` for everyone, and `/journal` is the journal in the language its visitor reads in | written under the language on a page asked for under it, and left as written at a plain address |
| `"address"` | `/uk/journal` is the journal in `uk`, and `/journal` is the journal in the first language for everyone | written under the language of the page it stands on |

A site in several languages says `"address"` or nothing. `"visitor"` is the
word of a site in one language, and a site that says it cannot serve a
second: before `set_site_languages` adds one, change `language` in
`site.json` on a branch and merge it.

A path that starts with the code of one of the other languages belongs to
that language. A page of the site's own there, such as `pages/uk.vzk`, is
refused.

## Catalogs

A catalog is one JSON object at the path of the file it translates, under
`locales/<code>/`, with `.json` for `.vzk`:

```text
pages/journal.vzk                     the page, in the first language
locales/uk/pages/journal.json         that page's words in Ukrainian
locales/uk/layouts/site.json          the words of layouts/site.vzk
locales/uk/components/PostCard.json   the words of components/PostCard.vzk
locales/uk/site.json                  the fields of site.json
```

The code is two or three lower-case letters and then, if anything, `-` and
two capitals: `uk`, `en`, `pt-BR`. The first language has no folder. A
catalog is written on a branch and goes live with a merge, like every other
file.

### A text is its own key

A text needs no name. A translation is found by the text itself, as a reader
reads it. Part of `pages/journal.vzk`:

```html
<head>
  <title>Journal</title>
</head>

<section id="journal">
  <h1>Notes from the bakery</h1>
  <p>New here? Read <a href="/journal/why-rye" class="link">why we bake with rye</a>.</p>
  <img src="/files/oven.jpg" alt="The oven at six in the morning">
  <p>Page {posts.page} of {posts.pages}</p>
</section>
```

And `locales/uk/pages/journal.json`:

```json
{
  "Journal": "Журнал",
  "Notes from the bakery": "Нотатки з пекарні",
  "New here? Read <a>why we bake with rye</a>.": "Уперше тут? Прочитайте, <a>чому ми печемо з жита</a>.",
  "The oven at six in the morning": "Піч о шостій ранку",
  "Page {page} of {pages}": "Сторінка {page} з {pages}"
}
```

- One key is one run of text with the inline elements inside it (`a`, `b`,
  `em`, `strong`, `span` and the like), so a sentence with a link in it is
  translated as one sentence.
- A tag in a key is its name alone: `<a>` stands for the file's own `<a>`
  with every attribute. A translation says where the element goes and what
  words it holds.
- `{page}` stands for the value the file writes there, named by the last
  name in it: `{posts.page}` is `{page}`, and `{post.title}` is `{title}`.
- A translation holds each tag and placeholder of its key and no other, so
  it never adds markup to a page.
- The `<title>`, the description, and the value of `alt`, `title`,
  `placeholder` and `aria-label` are texts like the others.
- A run with no letter of its own, such as `{post.published_at} · {post.minutes}`,
  has no key and stays as it is.
- What `<pre>`, `<textarea>` and `<svg>` hold, and what an element that says
  `translate="no"` holds, is not read for texts.

### Texts a script holds

A text written in a script is translated when it reaches the page unchanged,
in the catalog of the file that wrote it:

```html
<script server>
  const links = [
    { label: "Home", href: "/" },
    { label: "Journal", href: "/journal" },
  ]
</script>

<nav aria-label="Main">
  <a :for={link in links} href={link.href}>{link.label}</a>
</nav>
```

```json
{
  "Main": "Головне меню",
  "Home": "Головна",
  "Journal": "Журнал"
}
```

A text that is compared, joined or cut is never translated, so a translation
changes the words a visitor reads and nothing a script works out. A text of
a row, and what a visitor typed, are drawn as they are. A page's
`export const name` is translated by the page's own catalog, which is what
`site.pages` says in each language.

### Plurals

`plural(n, forms)` answers the form that fits a number, and `#` is where the
number goes:

```html
<p>{plural(posts.total, { one: "# post", other: "# posts" })}</p>
```

In a catalog a `plural` is one entry. Its key is the `other` form, and its
value holds the forms the language has:

```json
{
  "# posts": { "one": "# допис", "few": "# дописи", "many": "# дописів" }
}
```

### One text, two translations

When one text needs two translations, `text-key` gives the text of an
element a name, and the catalog translates it by that name after `@`:

```html
<button text-key="open-door">Open</button>
<p>The bakery is <span text-key="open-now">Open</span></p>
```

```json
{
  "@open-door": "Відчинити",
  "@open-now": "Відчинено"
}
```

### The fields of site.json and the labels of a table

`locales/uk/site.json` translates the texts among the fields of `site.json`.
Its keys are `@fields.<name>`, and it holds no other key:

```json
{
  "@fields.motto": "Хліб щоранку"
}
```

`locales/uk/tables/<handle>.json` holds what a table's fields are called in
that language, under the same keys. A refused form names a field by that
label.

### What is missing, stale or wrong

- A text with no entry is no finding, and is drawn in the first language.
- An entry whose key is no text of its file is stale, a warning that names
  the nearest text the file has: move the translation to the new text, or
  remove the entry. Changing a sentence in a `.vzk` file leaves its old
  entries stale, so update the catalogs in the same write.
- A catalog that is not one JSON object, or a translation that does not fit
  its key (a tag left out, a tag or a placeholder the key does not have),
  is an error and refuses the write.

## Links and the switcher

A file writes its addresses once, as they are in the first language. On a
page asked for under a language, a link to a page of the site is written
under it: `/journal` becomes `/uk/journal`. So are the addresses
`page.with(...)` answers, a pager's `next` and `previous`, and where a form
is sent. An address of a file, of another site, in `src`, or under `mailto:`
and `tel:` is never rewritten.

An element that says `as-written` keeps its link as the file writes it:

```html
<a href="/" as-written>Rye and Salt</a>
```

`page.locale` is the code of the language the page is asked in, and
`page.languages` lists each language the page is served in, the first one
first, each with its `locale`, the `url` this page is switched to it at, and
whether it is `current`. Use those addresses; do not build one:

```html
<nav aria-label="Languages">
  <a :for={language in page.languages} href={language.url} hreflang={language.locale}>{language.locale}</a>
</nav>
```

A list is never a condition, so ask `page.languages.length > 1` before
drawing a switcher. The platform writes the alternate tags of the head.

## Dates, numbers and forms

`formatDate` writes the names of months and of days in the language of the
page, and `formatNumber` and `formatMoney` write a number the way that
language does, so a file writes one pattern for every language. A file that
wants another pattern in another language picks it by `page.locale`.

The sentences of a refused form (`form.message`, `form.error.<field>`) are
held in English and Ukrainian. In any other language they are in English, so
a site in one writes a sentence of its own for each `code` it reads in
`form.errors`.

On a site in several languages an `<option>` of a form says its `value`:
translated words are not a choice the table knows.

## Rows

A row is in one language, and no catalog translates a row. Give the table a
`choice` field with the language role, mark it `filterable`, and write one
row per language. Give a post's translations the same slug, each row naming
its own language: the row's page then shows the row written in the language
being read, then one that names no language. A listing shows every language
unless the page narrows it with a `filter` on that field, comparing it with
`page.locale`.
