---
title: 'Translate UI strings'
description: 'Move hardcoded labels out of your templates into one table per language, and read them back with the t filter.'
slug: 'translate-ui-strings'
type: 'article'
date: 2026-08-18T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/how-to/translate-ui-strings/'
---

Every site accumulates labels that live in templates: "Read more", "Back to top", the words around a form. Reach for this when you want them in one place instead of scattered across partials, or when a second language means each one now needs two spellings.

This is not only for multilingual sites. The `t` filter is registered either way, and a single-language site gets the same benefit: one file to edit when the wording changes, instead of a search across templates.

---

## Prerequisites

- Baseline installed and building.
- Nunjucks templates, which is what Baseline configures by default.
- `defaultLanguage` (or `defaultLocale`) set in settings, even on a single-language site. It is the language `t` reads when a page has none of its own, and without it every lookup misses and renders its key.

---

## Create a table for your default language

```js
// src/_data/translations/en.js
export default {
	nav: { home: 'Home', about: 'About' },
	readMore: 'Read more'
};
```

Eleventy auto-loads `_data/`, and a nested directory becomes a nested key, so this file lands at `translations.en`. There is nothing to register.

---

## Swap one hardcoded label

Find a label in a template and pipe the key through `t`:

{% raw %}

```diff-nunjucks
-<a href="/about/">About</a>
+<a href="/about/">{{ "nav.about" | t }}</a>
```

{% endraw %}

Keys are dot-paths, so `nav.about` reads the nested key.

Build and check the page. If you see `nav.about` rendered where the label should be, the key is missing from the table, and Baseline has logged a warning naming it.

---

## Move the rest, one at a time

There is no shortcut here worth having. Move a label, build, look at the page. The failure mode is quiet if you move twenty at once and mistype one key, and obvious if you move one.

Group keys by where they appear rather than by what they say. `nav.home` and `footer.copyright` will still make sense to you in six months; `homeLink` and `copy1` will not.

---

## Fill in values that change

A `{name}` placeholder is replaced from the params you pass:

```diff-javascript
// src/_data/translations/en.js
export default {
	nav: { home: 'Home', about: 'About' },
	readMore: 'Read more',
+	greeting: 'Hello, {name}'
};
```

{% raw %}

```nunjucks
{{ "greeting" | t({ name: page.author }) }}
```

{% endraw %}

An unmatched placeholder stays visible as `{name}` rather than rendering blank, so a missing param shows up rather than leaving a gap you have to notice.

---

## Handle counts

Write the entry as an object of plural forms and pass a `count`:

```diff-javascript
// src/_data/translations/en.js
export default {
	nav: { home: 'Home', about: 'About' },
	readMore: 'Read more',
	greeting: 'Hello, {name}',
+	items: { one: '{count} item', other: '{count} items' }
};
```

{% raw %}

```nunjucks
{{ "items" | t({ count: results.length }) }}
```

{% endraw %}

Selection goes through `Intl.PluralRules`, so the categories are CLDR's rather than English's. A language with three forms needs three keys, and Polish is the usual example: `one` for 1, `few` for 2, `many` for 5.

---

## Add a second language

One file per language code, alongside the first:

```js
// src/_data/translations/nl.js
export default {
	nav: { home: 'Home', about: 'Over ons' },
	readMore: 'Lees meer'
};
```

Nothing else changes. Each page reads the table matching its own `page.lang`, so the templates you edited when you swapped that first label already work in both.

A key present in `en.js` and missing from `nl.js` falls back to the default language rather than breaking the page, which means a half-translated table ships something readable while you finish it.

---

## Notes

- **Override the language for a single call** with `{% raw %}{{ "nav.home" | t({ lang: 'fr' }) }}{% endraw %}`. Useful for a language switcher that names each language in its own words.
- **A key missing everywhere renders as the key**, and logs a warning. That is deliberate: a visible `nav.about` on the page is easier to catch than a blank space.
- **This is the string half of translation only.** Page relationships, sibling URLs and hreflang are the [[multilang | multilang module]]'s job, and `page.translations` is what you loop over for a language switcher.
- **Nunjucks specifically.** Add another template engine and `t` returns the key there rather than the string.

Full behaviour, including the lookup order and the language resolution, is in the [[filters | filters reference]].

---

## See also

- [[filters | Filters]] - `t` in full, lookup order included.
- [[multilang | Multilang module]] - page relationships, sibling URLs and hreflang, which `t` does not touch.
- [[multilingual-baseline-site | Build a multilingual site]] - the setup these strings sit on top of.
