# A history in the reader's language — keep what happened, say it when it is read

> A timeline, an audit trail or a "why is this here" list is read long after it is written, by readers in other languages. Store WHAT happened — its kind and its facts (who, which status, which queue, why) — never the sentence; say the sentence in a function when the history is read. Its words are then ordinary messages, translated, with plural forms, in whatever language the reader has. A sentence stored at the time stays in the language of whoever happened to cause it.

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

## Summary        {#summary}
Keep the event, not the words. A row says `Kind = "status"`, `Now = "Waiting on a refund"`; a function says
`$"Status set to {c.Now}."` when the row is shown. The compiler finds that sentence like any other text
([Which words are translated — the compiler finds every text a reader sees](https://osysharp.com/reference/i18n/extraction/)), so an Arabic reader reads it in Arabic and a Swedish reader in Swedish — from the same row. A
history whose sentences were stored reads in the language of whoever wrote it, forever.

## Signature      {#signature}
```osy syntax
entity Change { string Kind; string Now; … }                 // the facts, as they were at the time
string Said(Change c) => $"Status set to {c.Now}.";          // the words, said when read — a message like any other
Text(Said(change));                                          // in the reader's language
```

## Description    {#description}
**Why not store the sentence?** Because a sentence has a language, and the person who caused the event chose it, not
the person reading the history. A desk in Malmö and a desk in Riyadh read the same ticket; an event "Ines took it."
written at the time is English to both. Stored facts have no language: `Kind = "assigned"`, `To = "Ines"`.

**What to keep.** The kind, and every fact the sentence needs, *as it was at the time*: the name a status or a
colleague had then (a later rename does not rewrite history), the values before and after, the number of days, the
reason someone typed. Keep a closed reason as an enum (`HandoverReason.Moved`), never as its English label — the label is
read in the reader's language when it is shown (`reason.Label`).

**Where the words come from.** A function — a unit function, a class method, an override — whose result is shown. Every
literal, interpolation and `+` chain in it is a message with holes ([Which words are translated — the compiler finds every text a reader sees](https://osysharp.com/reference/i18n/extraction/)), so translations are checked
at compile time and a count chooses its plural form ([A count's plural forms — one text per CLDR category](https://osysharp.com/reference/i18n/plurals/)). A message's holes are filled with the stored
facts; a fact that is itself words (a reason somebody typed, a queue's name) is shown as it was written.

**Personal data in the facts stays classified.** A name in a history is a copy of who someone is; a value a customer
gave is their words. Give each its column, classified (`[Classification(Personal.Identity)]`,
`[Classification(Personal.Content)]`) — then erasing who someone is, restricting their data and keeping their words out
of the audit trail reach the history too. Inside a stored sentence they would have been reachable only as a whole.

**On the server or in the browser.** A function that reads only the row runs in the browser and says the sentence in the
browser's table. One that reads the app's settings, or another table, runs on the server, which says it in the
request's language — what the conversation and inbox kits do, so an app's own words can join them (below).

**The conversation and inbox kits work this way.** `Osysharp.Conversations` stores a `ConversationEvent` with its
facts and says it through `ConversationCopy.EventSentence`, which an app overrides for the kinds it records; a kind's
own facts ride `Facts` as a class of the app's own. `Osysharp.WorkQueues` keeps each step of an item's trail as typed facts
on its `Handover` and raises an event of the same facts ([[Event] — what happens to a record, declared once](https://osysharp.com/reference/function/event/)), which says itself — so a timeline keeps
the event (`Events.Write`) and says it when read (`Events.Read(…).Sentence()`):

```osy syntax
// The app records its own kind — facts passed IN, so the kit's own code writes it (a customer or a timer drives the
// bodies that write a timeline, and the kit's floor lets only the kit write past it).
Record(t.Conversation, "helpdesk.status", "Helpdesk", facts: JsonSerializer.Serialize(new TicketFacts { Name = status.Name }));

// …and says it, in the reader's language.
public class DeskCopy : ConversationCopy {
  public override string EventSentence(ConversationEvent e) {
    if (e.Kind == "helpdesk.status") { return $"Status set to {JsonSerializer.Deserialize<TicketFacts>(e.Facts!).Name}."; }
    if (e.Kind.StartsWith("work-queues.")) { return Events.Read(e.Facts!)?.Sentence() ?? base.EventSentence(e); }
    return base.EventSentence(e);
  }
}
```

## Examples       {#examples}
```osy title="a status history, said in the reader's language" test app=i18n-history
/// One change of a ticket's status — the facts, as they were.
entity StatusChange {
  [Required, MaxLength(40)] string Kind;
  [MaxLength(120)] string Was = "";
  [MaxLength(120)] string Now = "";
  int? Days;
  DateTime At = DateTime.UtcNow;
}

/// What a change says — said when it is shown, never stored.
string Said(StatusChange c) {
  if (c.Kind == "status") { return $"Status set to {c.Now} (was {c.Was})."; }
  if (c.Kind == "auto-solved") { return $"Solved after {c.Days ?? 0} days with no word from the customer."; }
  return "Something happened.";
}

[Route("/history")]
component History() {
  live var changes = StatusChange.OrderBy(c => c.At);
  render {
    Stack {
      foreach (var c in changes) { Text(Said(c)); }
    }
  }
}

translations sv {
  text "Status set to {c.Now} (was {c.Was})." => "Status satt till {c.Now} (var {c.Was}).";
  text "Solved after {value} days with no word from the customer." => {
    one: "Löst efter en dag utan svar från kunden.", other: "Löst efter {value} dagar utan svar från kunden." };
  text "Something happened." => "Något hände.";
}
```

## See also       {#see-also}
- [Which words are translated — the compiler finds every text a reader sees](https://osysharp.com/reference/i18n/extraction/) — which texts are messages, and how a function's words are found
- [A count's plural forms — one text per CLDR category](https://osysharp.com/reference/i18n/plurals/) — a count's forms, and why a ticket's number is not a count
- [A language's words — locales/<language>.osy](https://osysharp.com/reference/i18n/translations/) — the file the words are translated in
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — keeping a class of facts in one column
