# Translatable

> A text value per active language, stored in #configboxstrings — an EAV table, not a column on the model's own table.

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/translatable. Last updated 2026-08-25.

---
A text value per active language, stored in `#__configbox_strings` — an EAV table, not a column on
the model's own table.

**Storage kind:** `derived` · **Column:** none

Use it for **every** text a customer reads: product and question titles, answer labels, descriptions,
anything that appears in the storefront. Use [`string`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/string) for text that only ever reaches
the operator — an internal note, a SKU, a template name. Getting this wrong is expensive in one
direction only: a `string` that should have been translatable means a migration plus re-entry of
every value, whereas an over-translated internal field just shows one input per language.

## Settings

| Setting | Meaning |
|---|---|
| `langType` | **Required.** The `type` discriminator identifying this field's strings in `#__configbox_strings` |
| `defaultlabel` | Placeholder |
| `options` | `ALLOW_RAW` / `ALLOW_HTML`, and `USE_TEXTAREA` / `USE_HTMLEDITOR` for the widget |
| `editorWidth` / `editorHeight` | WYSIWYG size, as for `string`. Only with `USE_HTMLEDITOR` |

`langType` is an integer that must be **unique across the whole install** — it is the only thing
separating one field's strings from another's in a single shared table. Reusing a number silently
merges two fields' translations.

The form renders one input per active language, so a two-language install shows two boxes.

## What a record carries

Three kinds of key, for a property named `title`:

| Key | Holds |
|---|---|
| `title` | the **current** language's text |
| `title_en_GB` | the English text |
| `title_de_DE` | the German text |

One per **active** language. Because that set is per-install, the committed generated artifacts
describe only the bare key — a consumer cannot know the per-language keys from the schema alone and
must read the install's active languages.

## Building the key

```php
$key = KenedoLanguageHelper::getTranslationKey($this->propertyName, $tag);   // title_en_GB
```

**Never concatenate it yourself.** The key was assembled by hand in six places, and three of them
were only found by chasing a failing test — one of which, `checkForDuplicateUrlSegment()`, is silent
when the key does not resolve. A key built by concatenation has no single definition to grep for.

Keys used a hyphen (`title-en-GB`) until 2026-07-27. See the breaking-changes log.

## Storage

Reads `LEFT JOIN` `#__configbox_strings` on `type = langType AND key = <record id> AND language_tag
= <tag>`, and `appendDataForGetRecord()` adds the per-language keys.

Writes loop over the active languages and, per language:

- a non-empty text → `REPLACE INTO #__configbox_strings`
- an empty text → **`DELETE`** of that row

So an emptied translation does not become an empty string, it ceases to exist. Absence and
emptiness are the same state here, which is why there is no way to store a deliberately blank
translation distinct from an unset one.

`delete()` removes every language's row for the record, keyed on `type` + `key`. It does **not**
call the parent, which is harmless — a `derived` property has nothing external to clean up.

`check()` on a `required` translatable demands a value in **every** active language, not just the
current one. Adding a language to a live install therefore makes previously-valid records fail
validation the next time they are saved.

Reads are batched: `preload()` fetches many records' strings at once and `forget()` drops the cache,
which the model calls after any write. If you write to `#__configbox_strings` with hand SQL, call
`KenedoPropertyTranslatable::forget()` or subsequent reads in the same request serve stale text.

Because it is `derived`, this property contributes **no** column to the model's own table, type
generation excludes it from anything describing that table, and `storeExternally` does not apply.

@see string.md — for text the customer never sees
@see ../../migration-to-cb4/breaking-changes-log.md
@see ../../customization/com_configbox_language_overrides.md — overriding shipped texts
