Osy#betaRun it on Osyrin: osyrin.com

Reference / I18n

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

[Translatable] string Name;

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

stable1 example compiled by CIi18nlanguagetranslationdata

Summary#

An app's TEXT is translated in its locale files (A language's words — locales/<language>.osy); 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#

[Required, MaxLength(120), Translatable] string Name;
[MaxLength(4000), Translatable] string? Description;

Description#

Reading. In a language the app speaks (Which language a reader gets — and switching it — 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) 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) 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:

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

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#

Related

A language's words — locales/<language>.osy

A project's translations are checked-in source: one `locales/<language>.osy` per language, a `translations sv { }`…

Which language a reader gets — and switching it

A reader's language is, in order: their explicit choice (a `/sv/` address prefix or a language switch), their own…

The Translations desk — Osysharp.Localization

`use Osysharp.Localization@0;` gives an app a translator's desk: its words and its `[Translatable]` data in each…

Which words are translated — the compiler finds every text a reader sees

You write your app's words where they belong, in ordinary strings. The compiler finds every one a reader can see — by…