# [Translatable] — an entity's own words in every language

> `[Translatable]` on a stored `string` member keeps a translation per language, in a table beside its entity. Code reads, filters, sorts and writes the member exactly as any string: in a language the app speaks it sees that language's text (falling back to the stored one), sorts it in that language's order, and a write in it writes the translation.

<!-- id: i18n-translatable · area: i18n · stability: stable · html: https://osysharp.com/reference/i18n/translatable/ -->

## Summary        {#summary}
An app's TEXT is translated in its locale files ([A language's words — locales/<language>.osy](https://osysharp.com/reference/i18n/translations/)); its DATA — a product's name, a shelf's blurb, a
plan's description — is translated with `[Translatable]`. The member stays an ordinary `string`: nothing about the code
that reads, filters, sorts or writes it changes.

## Signature      {#signature}
```osy syntax
[Required, MaxLength(120), Translatable] string Name;
[MaxLength(4000), Translatable] string? Description;
```

## Description    {#description}
**Reading.** In a language the app speaks ([Which language a reader gets — and switching it](https://osysharp.com/reference/i18n/language/) — the request's, or one set with
`Session.SetLanguage`), a read of the member is its translation in that language, else the stored value. Everywhere:
a page's `live` query, a function body, a filter (`Product.Where(p => p.Name.Contains(q))` searches the Swedish names in
Swedish), an `OrderBy` — which sorts in that language's order (Swedish puts Å, Ä, Ö after Z, Å before Ä) in the database,
in memory and in the browser alike. The source language reads the stored value, in its own order. With no language in
effect (an app with one language, a workflow) nothing changes at all.

**Writing.** A write in a language the app speaks writes that language's TRANSLATION and leaves the stored value as it
is — what a reader reads is what they write back, so a desk in Swedish edits the Swedish text. A new row's text is its
stored value, in whatever language it was written. An empty text removes a translation. A write lands in the language in
effect when it is SAVED, so a server body that switches language with `Session.SetLanguage` must save first — a switch
with unsaved translatable rows is refused, naming `UnitOfWork.Commit()`.

**Stale.** Each translation keeps the stored text it was written against. When the stored text is edited later, the
translation is kept and is STALE — the Translations desk ([The Translations desk — Osysharp.Localization](https://osysharp.com/reference/i18n/desk/)) lists it for the translator.

**Where translations are kept.** Not on the row: in one table the compiler adds to the app the first time any entity
(yours or a package's) declares a `[Translatable]` member — one row per row, member and language, holding the text and
the stored text it was written against. A read in a language looks up that one language's text for each row it reads,
by an index on (row, member, language), so a catalogue kept in 48 languages costs a reader of one exactly what one
language costs — the same single query, and nothing stored on the rows themselves. Nothing in your code names this
table: it is the `Osysharp.Translatable` capability, which no app writes a `use` for — the attribute brings it. An app compiled while translations were still kept on the row has them moved into it by its next compile.

**Security.** A translation is readable exactly where the value it translates is. Read as the member (`p.Name` in
Swedish) it IS that member's value — the row's read rule selects it or not, and a `deny read` on the member masks its
translation too. The translations table has no rules of its own to write: each row's access is derived from the row it
translates — that entity's own `security { }` rules — and from the member's field rules, so a translation of a member
masked from a reader is hidden from them there as well (the Translations desk reads it that way). Writing one is an
update of that member of that row, under its update rules.

**Set-based writes.** `Update(…)` and a single-row write in a language write translations like any other write. An
upsert (`Insert(…, onConflict: …)`, [Insert](https://osysharp.com/reference/query/insert-from/)) whose merge would assign a `[Translatable]` member while the
work is in a language other than the source is refused with nothing written — update those rows instead.

**Seeding a second language** — a data file with `"language"`, the rows' key and only their translatable members:

```json title="data/sv/02-products.json" syntax
{ "entity": "Product", "key": "Slug", "language": "sv",
  "rows": [ { "Slug": "kiambu-aa", "Summary": "Svarta vinbär, grapefrukt, farinsocker." } ] }
```

`osy import` writes those values as Swedish translations. It creates nothing: a row whose key matches nothing, a member
that is not `[Translatable]`, and a key that is, are each refused. A lookup may name a key's row
(`"lookups": { "Product": "Product[Slug]" }`).

**What is refused at compile time:** `[Translatable]` on anything but a stored `string` (a number, a reference, a computed
member), and beside `[ConcurrencyCheck]` — a reader in another language reads the translation, so the check would
compare it with the stored text.

**Leave identifiers alone.** A slug, a SKU, a code: they are how the app finds rows, and a slug that changed with the
language is a link that breaks when a Swedish reader shares it.

## Examples       {#examples}
```osy title="the same function, in English and in Swedish" test app=i18n-translatable
entity Dish {
  [Required, MaxLength(80), Translatable] string Name;
  [Required, Unique, MaxLength(20)] string Code;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

string Menu() {
  return string.Join(", ", Dish.OrderBy(d => d.Name).Select(d => d.Name).ToList());
}

void GiveTheMenuSwedishWords() {
  UnitOfWork.Commit();
  Session.SetLanguage("sv");
  foreach (var d in Dish.ToList()) {
    if (d.Code == "bun") { d.Name = "Kanelbulle"; }
    if (d.Code == "soup") { d.Name = "Ärtsoppa"; }
  }
  UnitOfWork.Commit();
}
```

## See also       {#see-also}
- [The Translations desk — Osysharp.Localization](https://osysharp.com/reference/i18n/desk/) — the Localization kit: what is missing or stale, and where people fix it
- [A language's words — locales/<language>.osy](https://osysharp.com/reference/i18n/translations/) — the app's own words, in locale files
- [Which language a reader gets — and switching it](https://osysharp.com/reference/i18n/language/) — which language a request reads in
