Osy#betaRun it on Osyrin: osyrin.com

Reference / I18n

The Translations desk — Osysharp.Localization

use Osysharp.Localization@0; TranslationsDesk() · Translation.Texts(language) · Translation.Data(language, everything, page, pageSize) · Translation.DataPages(language, everything, pageSize) · Translation.Problems(id, text) · Translation.SetData(entity, row, field, language, text)

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

stable1 example compiled by CIi18nlanguagetranslationkit

Source on GitHub: Osysharp.Localization — github.com/osysharp/localization ↗

Summary#

The locale files are the BASE of an app's words (A language's words — locales/<language>.osy). The Localization kit is where people change them afterwards — and translate the app's data ([Translatable] — an entity's own words in every language) — without a developer, a recompile or a deploy.

Signature#

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#

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) — 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). 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:

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

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;
}
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#

Related

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

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

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

osy translate — the request for any model, and its answer read back

There is no platform model. `osy translate --to sv,de` brings `locales/sv.osy` and `locales/de.osy` in step with the…

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…