Osy#betaRun it on Osyrin: osyrin.com

Reference / I18n

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

Text("…") · Button("…") · label: / placeholder: / alt: · $"{n} items" · throw new Exception("…") · Translate("…", key: "…")

You write your app's words where they belong, in ordinary strings. The compiler finds every one a reader can see — by where it goes, not by what it looks like — with its context (page, role, neighbours, placeholders), and each is shown in the reader's language. A message is identified by its source text and its role; `Translate("…", key: "…")` splits one text that needs two translations.

stable1 example compiled by CIi18nlanguagetranslationui

Summary#

There is nothing to wrap and no key to invent. Write Text("Your bag is empty."), Button("Check out", …), Field("Email", …) and throw new Exception("That code is not one of ours.") as always: the compiler finds each text a reader sees and shows it in the reader's language (from locales/<language>.osy, A language's words — locales/<language>.osy), falling back to the text you wrote — never to nothing.

Signature#

Text("…")                                  // any literal in a text position is a message
Text($"{count} items in your bag")         // an interpolation is ONE message with a hole: "{count} items in your bag"
Text("Remove " + line.Name)                // so is a + chain: "Remove {line.Name}"
throw new Exception("…");                  // an exception's message
Translate("Open", key: "shop-status-open") // the explicit form, and the one way to give a text a key

Description#

What counts is where the text goes. A string is a message when it reaches a place a reader sees it:

  • an atom's text — Text, a Button's or Pressable's content, an accessible name (label:), a placeholder:, an image's alt:; in an [Email] component the mail's subject =>, its text, its links and an Editable's default:;
  • a component parameter that reaches one — worked out through every component, so Button("Save"), Field("Email") and your own SectionLabel("Orders") are text because of what those components do with the value, while a string href is not; a text parameter's default counts too;
  • a class field read into one — new NavItem { Label = "Coffee" } is text because the shell shows item.Label;
  • a function's return when the function is called in one — Text(DeliveryLine(w)) translates the words DeliveryLine puts together, including its locals;
  • a component field assigned in an action and shown — error = "Try again";;
  • every exception's message — which exceptions reach a screen is not decidable (any action may catch and show ex.Message), so all are extracted, except a message that quotes code in backticks: that one is addressed to the developer who configured something, not to the reader;
  • a page's [Title("…")] and its head { Title = …; Description = …; };
  • an enum member's declared [Label("Draft")] (role label, the enum's other labels beside it) — read in the reader's language wherever the label is shown, a badge, a dropdown, status.Label. A member with no [Label] shows its NAME (Pending), and that name is a word to translate too when readers see the enum — when it declares a [Label] on any member, or one of the app's own entities stores it — and the name is a word rather than a CamelCase identifier (SecondStep wants a [Label]). An enum only code reads (Direction.Up) is not text;
  • an entity field's display name — its [Label("Solved at")], or its name in words ("Arrived at") on an entity a report's field picker reaches — what a report calls the field ([Label] on a field: what a report calls it, in the reader's language);
  • a validation attribute's own sentence — [Required("Give it a name.")], [MaxLength(80, "…")], [Pattern(regex, "…")], [Unique("…")] … (role validation) — what the browser and the server say when the rule refuses a value. The regex and the bounds are not words.

What is left alone: a string that goes nowhere a reader sees (an address, a key, a CSS value, an identifier), a value the code COMPARES with == (translating it would break the comparison — unless its name says it is display text: Label, Title, Caption …), a text with no letter in it ("·", "×"), anything in a test, and an entity's data — a product's name is data, and data is translated with [Translatable] ([Translatable] — an entity's own words in every language).

A sentence is one message. $"{n} items in your bag" and "Remove " + name are each one message with holes, so a translator can move the hole where their language puts it. A hole that is itself text — {(reminder ? ", and one reminder" : "")} — is a message of its own.

Identity is the text and its role — button "Save" and text "Save" are two messages; "Save" on two pages is one. The place is context, not identity: moving a button does not make its label new. When one English text needs two translations (an "Open" that is a shop's hours and an "Open" that is a button), give ONE of them a key with Translate("Open", key: "shop-status-open"). Translate("…") also marks a text the analysis cannot see — a name a class keeps and the code compares elsewhere.

The browser receives only its language's table, by content address, and a text's words are never markup: they are shown as text wherever they land.

Examples#

public sealed class Crumb {
  public string Label;
  public string To = "";
}

[Composable]
component Heading(string title, string href) {
  render { Link(href: href) { Text(title); } }
}

string Notice() => "Roasted every Tuesday.";

[Route("/bag")]
[AllowAnonymous]
[Title("Your bag")]
component BagPage() {
  int count = 3;
  string status = "";
  Crumb home = new Crumb { Label = "Home", To = "/" };
  action Save() { status = "Saved."; }
  render {
    Stack {
      Heading("Your bag", href: "/bag");          // "Your bag" — a title, through Heading's parameter; "/bag" is not text
      Text($"{count} items in your bag");         // one message, with the hole {count}
      Text(Notice());                             // a function's words
      Text(status);                               // "Saved." — assigned in the action
      Link(href: home.To) { Text(home.Label); }   // "Home" — a class field shown as text
      Button("Save", onPress: Save);
      Text(Translate("Open", key: "shop-status-open"));   // the one "Open" that needs its own translation
    }
  }
}

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 { }`…

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

A count's plural forms — one text per CLDR category

A message with a whole-number hole (`$"{n} items"`) may take one text per plural category of its language — CLDR's…

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

Culture formatting — ToString(format, culture)

Format numbers, currency, percentages and dates for a declared culture — `total.ToString("C", "sv-SE")` → `1 234,56…