mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(chord): i18n proposal, t.message and one file per host
This commit is contained in:
parent
48f9f1b423
commit
1746cfd515
2 changed files with 47 additions and 4 deletions
|
|
@ -117,6 +117,24 @@ export default defineFacet((_host, self) => {
|
|||
- A plugin's own catalogs are its built-in contributions; a translation plugin can also override one of them (a
|
||||
better German), contributed explicitly with `override: true`.
|
||||
|
||||
## One file, every host
|
||||
|
||||
Registries are per host: a contribution lives on the host whose facet made it. A French catalog contributed from the
|
||||
web facet exists in browser pages only. So a plugin keeps its messages in one shared file, `messages.ts`, which every
|
||||
facet that renders text imports (the web and TUI facets; the worker's too when it builds messages with `t.message`),
|
||||
and a translation plugin keeps its catalog in one shared file too and contributes it from each facet that renders:
|
||||
|
||||
```ts
|
||||
// plugins/french/fr.ts: the catalogs, written once
|
||||
export const notesFr: MessagesOf<typeof notes> = { newNote: "Nouvelle note", /* ... */ };
|
||||
// plugins/french/web.ts and plugins/french/tui.ts: the same two lines each
|
||||
export default defineFacet((_host, self) => self.contribute(Translations, { namespace: "notes", locale: "fr", messages: notesFr }));
|
||||
```
|
||||
|
||||
The server and workers need catalogs only when they produce final text themselves (an email, a push notification);
|
||||
then their facets contribute them too, and the text is rendered in the recipient's language from their preference.
|
||||
Catalogs never travel over links: they contain functions.
|
||||
|
||||
# Choosing a Language {#locale lede="Per viewer, defaulted from the device, switched live."}
|
||||
|
||||
The language is a user preference, kept with the user's other preferences (the kit's per-principal document, like
|
||||
|
|
@ -148,9 +166,10 @@ So objects carry messages as data, and each client renders them:
|
|||
// plugins/notes/contract.ts: a member of the Notes object's state holds a message as data
|
||||
lastSaved: opt(obj({ namespace: str(), key: str(), params: arr(unknown()) })),
|
||||
|
||||
// plugins/notes/worker.ts: the object writes the key and its parameters, never a sentence
|
||||
// plugins/notes/worker.ts: the object writes the key and its parameters, never a sentence; the same `t` builds it
|
||||
import { t } from "./messages.ts";
|
||||
await self.tx(async (tx) => {
|
||||
(await tx.draft(status)).lastSaved = { namespace: "notes", key: "saved", params: [note.title, note.at] };
|
||||
(await tx.draft(status)).lastSaved = t.message("saved", note.title, note.at); // typed: key and arguments checked
|
||||
}, ctx);
|
||||
|
||||
// plugins/notes/web.tsx: each viewer renders it in its own language
|
||||
|
|
@ -158,6 +177,12 @@ const notes = useHold(Notes, { session });
|
|||
const text = notes.lastSaved && t.render(notes.lastSaved); // typed against the notes catalog
|
||||
```
|
||||
|
||||
`t.message` returns a `Message<"notes">`, plain data `{ namespace, key, params }`, typed against the first catalog:
|
||||
`t.message("saved", 42)` and `t.message("svaed", ...)` are type errors. `t.render` takes only `Message<"notes">`. Across a
|
||||
link the contract's member is a generic message schema, so `t.render` checks the namespace and key at runtime and
|
||||
shows the key for an unknown one. The writer and the reader are type-checked; the hop between them is checked at
|
||||
runtime.
|
||||
|
||||
The same holds for anything else that reaches people: an app's notices object stores `{ namespace, key, params }`
|
||||
per notice, a tool card's summary carries one, an error shown to a user carries a code and parameters.
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,18 @@ registry rule for conflicts).</li>
|
|||
<li>A plugin's own catalogs are its built-in contributions; a translation plugin can also override one of them (a
|
||||
better German), contributed explicitly with <code>override: true</code>.</li>
|
||||
</ul>
|
||||
<h3 id="one-file-every-host">One file, every host</h3>
|
||||
<p>Registries are per host: a contribution lives on the host whose facet made it. A French catalog contributed from the
|
||||
web facet exists in browser pages only. So a plugin keeps its messages in one shared file, <code>messages.ts</code>, which every
|
||||
facet that renders text imports (the web and TUI facets; the worker's too when it builds messages with <code>t.message</code>),
|
||||
and a translation plugin keeps its catalog in one shared file too and contributes it from each facet that renders:</p>
|
||||
<pre><code><span class="tk-c">// plugins/french/fr.ts: the catalogs, written once</span>
|
||||
<span class="tk-k">export</span> <span class="tk-k">const</span> notesFr: <span class="tk-t">MessagesOf</span><<span class="tk-k">typeof</span> notes> = { newNote: <span class="tk-s">"Nouvelle note"</span>, <span class="tk-c">/* ... */</span> };
|
||||
<span class="tk-c">// plugins/french/web.ts and plugins/french/tui.ts: the same two lines each</span>
|
||||
<span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) => self.<span class="tk-f">contribute</span>(<span class="tk-t">Translations</span>, { namespace: <span class="tk-s">"notes"</span>, locale: <span class="tk-s">"fr"</span>, messages: notesFr }));</code></pre>
|
||||
<p>The server and workers need catalogs only when they produce final text themselves (an email, a push notification);
|
||||
then their facets contribute them too, and the text is rendered in the recipient's language from their preference.
|
||||
Catalogs never travel over links: they contain functions.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="locale"><header class="opener"><div class="numeral">III</div><h2>Choosing a Language</h2><p class="lede">Per viewer, defaulted from the device, switched live.</p></header>
|
||||
|
|
@ -242,14 +254,20 @@ both look at the same object state.</p>
|
|||
<pre><code><span class="tk-c">// plugins/notes/contract.ts: a member of the Notes object's state holds a message as data</span>
|
||||
lastSaved: <span class="tk-f">opt</span>(<span class="tk-f">obj</span>({ namespace: <span class="tk-f">str</span>(), key: <span class="tk-f">str</span>(), params: <span class="tk-f">arr</span>(<span class="tk-k">unknown</span>()) })),
|
||||
|
||||
<span class="tk-c">// plugins/notes/worker.ts: the object writes the key and its parameters, never a sentence</span>
|
||||
<span class="tk-c">// plugins/notes/worker.ts: the object writes the key and its parameters, never a sentence; the same `t` builds it</span>
|
||||
<span class="tk-k">import</span> { t } <span class="tk-k">from</span> <span class="tk-s">"./messages.ts"</span>;
|
||||
<span class="tk-k">await</span> self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) => {
|
||||
(<span class="tk-k">await</span> tx.<span class="tk-f">draft</span>(status)).lastSaved = { namespace: <span class="tk-s">"notes"</span>, key: <span class="tk-s">"saved"</span>, params: [note.title, note.at] };
|
||||
(<span class="tk-k">await</span> tx.<span class="tk-f">draft</span>(status)).lastSaved = t.<span class="tk-f">message</span>(<span class="tk-s">"saved"</span>, note.title, note.at); <span class="tk-c">// typed: key and arguments checked</span>
|
||||
}, ctx);
|
||||
|
||||
<span class="tk-c">// plugins/notes/web.tsx: each viewer renders it in its own language</span>
|
||||
<span class="tk-k">const</span> notes = <span class="tk-f">useHold</span>(<span class="tk-t">Notes</span>, { session });
|
||||
<span class="tk-k">const</span> text = notes.lastSaved && t.<span class="tk-f">render</span>(notes.lastSaved); <span class="tk-c">// typed against the notes catalog</span></code></pre>
|
||||
<p><code>t.message</code> returns a <code>Message<"notes"></code>, plain data <code>{ namespace, key, params }</code>, typed against the first catalog:
|
||||
<code>t.message("saved", 42)</code> and <code>t.message("svaed", ...)</code> are type errors. <code>t.render</code> takes only <code>Message<"notes"></code>. Across a
|
||||
link the contract's member is a generic message schema, so <code>t.render</code> checks the namespace and key at runtime and
|
||||
shows the key for an unknown one. The writer and the reader are type-checked; the hop between them is checked at
|
||||
runtime.</p>
|
||||
<p>The same holds for anything else that reaches people: an app's notices object stores <code>{ namespace, key, params }</code>
|
||||
per notice, a tool card's summary carries one, an error shown to a user carries a code and parameters.</p>
|
||||
<p>Parameters are plain data (strings, numbers, ISO dates), so they cross links like any state. What is never
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue