Skip to content

A vzk site in several languages

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.

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.

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

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 needs no name. A translation is found by the text itself, as a reader reads it. Part of pages/journal.vzk:

<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:

{
"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.

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

<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>
{
"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.

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

<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:

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

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 @:

<button text-key="open-door">Open</button>
<p>The bakery is <span text-key="open-now">Open</span></p>
{
"@open-door": "Відчинити",
"@open-now": "Відчинено"
}

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

Section titled “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:

{
"@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.

  • 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.

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:

<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:

<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.

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.

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.