# The Translations desk — Osysharp.Localization

> `use Osysharp.Localization@0;` gives an app a translator's desk: its words and its `[Translatable]` data in each language it speaks, what is not translated yet or went stale, and staff overrides of any word — kept through every recompile, and served at once. The `Translation.*` verbs underneath are the platform's, and run as the caller.

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

## Summary        {#summary}
The locale files are the BASE of an app's words ([A language's words — locales/<language>.osy](https://osysharp.com/reference/i18n/translations/)). The Localization kit is where people change
them afterwards — and translate the app's data ([[Translatable] — an entity's own words in every language](https://osysharp.com/reference/i18n/translatable/)) — without a developer, a recompile or a deploy.

## Signature      {#signature}
```osy syntax
use Osysharp.Localization@0;                                   // in app { }
policy EditsTranslations => IsStaff;                          // the kit's one question: who edits the words
[Route("/staff/translations")] [Authorize(IsStaff)]
component StaffTranslations() { render { TranslationsDesk(); } }

// Optional: the places staff WRITE words once per language (a mail's paragraph) — the desk lists what is missing.
public interface IWrittenWords { List<WrittenWords> Written(); }
public class WrittenWords { string Where; List<string> Languages; string? WriteAt; }
app.Translations = new TranslationsSetup { Written = [ new MyMailWords() ] };
```

## Description    {#description}
**Two kinds of words, one desk.** *Words* lists every text the compiler found ([Which words are translated — the compiler finds every text a reader sees](https://osysharp.com/reference/i18n/extraction/)) — a button, a
heading, an enum's label, a validation sentence, a mail's subject — with where it is and what sits beside it, because a
word cannot be translated well without knowing whether it is a button or a heading. *Data* lists every
`[Translatable]` value of every row the reader may read, a page of rows at a time (a catalogue can be large). By
default each shows only what needs doing: **not translated**, or **out of date** because its source changed — and for
*Data* that is decided in the database, so the pages are pages of the work still owed, not of the whole catalogue.

**Overrides survive a recompile.** A compile loads the checked-in locale files as the base, with their context; an
override is a `TextOverride` row — DATA — so a recompile refreshes the base and never touches it. When a recompile
changes a text's source, its identity changes too: the old override is kept, shown out of date beside the text that
replaced it, and no longer served. Readers see an override the moment it is saved (every server re-reads within a
minute). A text with a count (one form per plural category) is translated in the locale file.

**Words staff write per language, elsewhere.** Some words are neither the app's text nor a row's `[Translatable]`
value: staff write them afresh in each language somewhere else in the app — a mail's paragraph changed at a shop's
desk, kept per language because a Swedish customer is mailed the Swedish words ([[Email] — mail rendered from data](https://osysharp.com/reference/ui/email/)). The desk cannot know
where an app keeps those, so the app lists each place in `app.Translations` — a class implementing `IWrittenWords`,
whose `Written()` answers every piece with the languages it is written in — and a third tab, *Written by staff*, lists
each one written in some of the app's languages and not in all, in every direction (Swedish only is missing its English
as much as the reverse), linking to `WriteAt` where staff write it. `TranslationWritten(everything)` answers the same
list for a desk of your own.

**Who may.** Whoever the app's one line admits — `policy EditsTranslations => IsStaff;`; unanswered, nobody. Listing the app's
words asks the same question — only a caller who may create a `TextOverride` may list them, because they include the
words of signed-in pages and of mail, which an anonymous visitor is never served. A data translation is written as an
update of that member of that row, under the writer's own rules.

**The verbs**, for a desk of your own — all server effects, all as the caller:

| verb | answers |
|---|---|
| `Translation.Texts(language)` | `List<TranslationText>` — every text: `Id`, `Role`, `Source`, `Where`, `Neighbours`, `Holes`, `MaxLength`, `Plural`, and the locale file's `Base` (and whether it is `BaseStale`) in that language |
| `Translation.Problems(id, text)` | `""`, or what is wrong with `text` as that message's translation: a placeholder dropped or invented, markup, nothing at all |
| `Translation.Data(language, everything, page, pageSize)` | `List<DataTranslation>` — one page (from 0, `pageSize` rows of one entity, 50 when left out) of the `[Translatable]` values the caller may read: `Entity`, `Row`, `Field`, `Label`, `Source`, `Text` ("" when missing), `Stale`. `everything: false` pages only rows with something to do; `Translation.Data(language)` is the first page of every row |
| `Translation.DataPages(language, everything, pageSize)` | `int` — how many pages `Translation.Data` has |
| `Translation.SetData(entity, row, field, language, text)` | writes one data translation ("" removes it); a text equal to the source is kept as its translation |

## Examples       {#examples}
```osy title="a desk of your own: what is still owed in one language" test app=i18n-desk
int OwedIn(string language) {
  var words = Translation.Texts(language).Where(t => !t.Plural && (t.Base == "" || t.BaseStale)).Count();
  var data = 0;
  for (var page = 0; page < Translation.DataPages(language, false, 50); page++) {
    data += Translation.Data(language, false, page, 50).Where(d => d.Text == "" || d.Stale).Count();
  }
  return words + data;
}

string CheckFirst(string id, string text) {
  var problem = Translation.Problems(id, text);
  return problem == "" ? "fine" : problem;
}
```

```osy title="a shop's mail words on the desk: each paragraph, with the languages staff wrote it in" syntax
public class ShopMailWordsOnTheDesk : IWrittenWords {
  public List<WrittenWords> Written() {
    var found = new List<WrittenWords>();
    foreach (var w in ShopMailWords.OrderBy(w => w.Template + "/" + w.Part).ToList()) {
      var where = $"Mail · {w.Template} · {w.Part}";
      var item = found.FirstOrDefault(f => f.Where == where);
      if (item == null) {
        item = new WrittenWords { Where = where, WriteAt = "/staff/desk" };
        found.Add(item);
      }
      item.Languages.Add(w.Language);
    }
    return found;
  }
}

app.Translations = new TranslationsSetup { Written = [ new ShopMailWordsOnTheDesk() ] };
```

## See also       {#see-also}
- [[Email] — mail rendered from data](https://osysharp.com/reference/ui/email/) — a mail's editable words, kept per language
- [[Translatable] — an entity's own words in every language](https://osysharp.com/reference/i18n/translatable/) — which data is translated, and how code reads it
- [A language's words — locales/<language>.osy](https://osysharp.com/reference/i18n/translations/) — the checked-in base
- [osy translate — the request for any model, and its answer read back](https://osysharp.com/reference/i18n/translate/) — `osy translate`, the command-line round trip
