Skip to main content
Version: 4.0 preview

Translatable

Version
4.0 preview
Updated
View markdown

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

SettingMeaning
langTypeRequired. The type discriminator identifying this field's strings in #__configbox_strings
defaultlabelPlaceholder
optionsALLOW_RAW / ALLOW_HTML, and USE_TEXTAREA / USE_HTMLEDITOR for the widget
editorWidth / editorHeightWYSIWYG 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:

KeyHolds
titlethe current language's text
title_en_GBthe English text
title_de_DEthe 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

$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