Source guide/localization.simd

1# Localization
2
3Translating a program's user-facing messages is a problem most
4languages solve with a gettext-like library: strings live in an
5external catalogue, translators edit them, your code looks them
6up at runtime. Silo follows that general shape but replaces the
7runtime lookup with **compile-time code generation** — the
8compiler reads your message catalogue, generates typed words from
9it, and checks every translation against the fallback catalogue
10for consistency.
11
12The format is `.sil`, and it sits next to your `.si` source files.
13
14## A `.sil` file
15
16A catalogue is a list of message definitions. Each message starts
17with `:name`, lists any typed placeholders in braces, then gives
18the message text ending with `;`:
19
20```silo
21# app.sil
22
23:greeting {name} "Hello, {name}!" ;
24
25@context("title") @max-length40 @note("Keep short — narrow header")
26:settings-title "Settings" ;
27
28# Typed placeholder — Int enables CLDR plural selection
29:item-count {count Int}
30  | one "{count} item"
31  | _   "{count} items" ;
32
33:price {amount Decimal} "Total: {amount Currency}" ;
34:receipt {id Int} "Your receipt ID is {id Hex}" ;
35
36@context("button")
37:save-button "Save"
38  .loading "Saving..."
39  .done    "Saved!" ;
40
41@context("label")
42:login "Log in to {app-name}."
43  .placeholder "user@example.com"
44  .aria-label  "Login email field" ;
45```
46
47A few things to notice:
48
49- **Typed placeholders.** `{name}` is a default `Str`;
50  `{count Int}` constrains the placeholder's type to `Int`, which
51  also switches the compiler into CLDR plural-selection mode.
52  Other types — `Float`, `Decimal`, `Bool`, `Date`, `Time`,
53  `DateTime` — are the **closed** set. Anything else is treated
54  as `Str`.
55- **Format traits in holes.** `{amount Currency}` and `{id Hex}`
56  apply a format trait the same way the format macro does
57  ([chapter 13](./13-strings.simd)). Locale-aware traits consult
58  the `CurrentLocale` aspect.
59- **Annotations** — `@context(...)`, `@note(...)`, `@max-length`
60  — give translators context. They don't affect the runtime
61  output, but they're part of the catalogue so the same notes
62  follow a message everywhere.
63- **Attributes** — `.loading`, `.done`, `.placeholder` — are
64  companion strings attached to a parent message. Generated as
65  record fields so callers can reach them with `.fieldname`.
66
67## Translations
68
69A locale-specific file is named `<stem>.<locale>.sil`:
70
71```silo
72# app.fi-FI.sil
73.locale "fi-FI"
74
75:greeting {name} "Hei, {name}!" ;
76:settings-title "Asetukset" ;
77:item-count {count Int}
78  | one "{count} tiedosto"
79  | _   "{count} tiedostoa" ;
80:price {amount Decimal} "Yhteensä: {amount Currency}" ;
81:save-button "Tallenna"
82  .loading "Tallennetaan..."
83  .done    "Tallennettu!" ;
84```
85
86A translation file can be **partial**: it only needs to include
87the messages it translates. Missing messages fall back to the
88base catalogue, so nothing breaks if a translator hasn't got to
89every string.
90
91## What the compiler generates
92
93For each message in the fallback catalogue, the compiler generates
94a typed word. The stack effect mirrors the placeholder types:
95
96```silo
97:fn greeting    ( Str -> Str ) ... :end     # one placeholder, :Str
98:fn item-count  ( Int -> Str ) ... :end     # one placeholder, :Int
99:fn price       ( Decimal -> Str ) ... :end
100:fn settings-title ( -> Str ) ... :end      # no placeholders
101```
102
103Calling them works like any other word:
104
105```silo
106"Alice" greeting               # ⌊"Hello, Alice!"⌉ (default locale)
107
108'fi-FI (Locale .try-from) .unwrap :with CurrentLocale
109  "Alice" greeting             # ⌊"Hei, Alice!"⌉
110  5 item-count                 # ⌊"5 tiedostoa"⌉
111:end
112```
113
114Locale selection runs through the
115[`CurrentLocale` aspect](./11-aspects.simd) — the same aspect
116that `Decimal` / `Currency` / `DateTime` format traits consult.
117`:with` overrides for a scope; without a `:with`, you get the
118locale the host installed at startup.
119
120## Locale
121
122`Locale` is a real ICU4X locale value (language, script, region,
123variant, Unicode extensions) — not a symbol tag. You build one
124via `(Locale .try-from)` from a BCP-47 `Symbol` or `Str`:
125
126```silo
127'de-DE (Locale .try-from) .unwrap pop-> de
128"en-GB" (Locale .try-from) .unwrap pop-> en
129```
130
131Invalid tags return `Err` at runtime; the compiler can't catch
132that because tags are usually parameters, not literals. `CurrentLocale`
133takes a `Locale` directly — the aspect's signature is
134`(CurrentLocale Locale)`.
135
136## Selectors
137
138For `Int`-typed placeholders, the catalogue uses **CLDR plural
139categories** — `zero`, `one`, `two`, `few`, `many`, `other` — and
140the compiler fills in whichever ones each locale requires.
141
142Multi-selector messages (selecting on more than one placeholder)
143list arms as comma-separated patterns:
144
145```silo
146:delete-confirm {count Int} {gender}
147  | one masculine  "Delete him?"
148  | one feminine   "Delete her?"
149  | one _          "Delete it?"
150  | _   _          "Delete {count} items?" ;
151```
152
153Silo's selectors:
154
155- **`Int`** — CLDR plural categories.
156- **`Str`** — exact match against the arm's string literal.
157- **`Bool`** — exhaustive `true`/`false`.
158- **`_`** — wildcard, matches anything.
159
160## Attributes are methods, each with its own signature
161
162When a message carries attributes, the compiler generates a
163**marker record** plus an `:impl` block where the main message
164and every attribute is its own method. Each method takes only
165the placeholders it actually uses — an attribute with different
166placeholders from the main message gets a different signature
167entirely, and an attribute with no placeholders is nullary even
168when the main message takes arguments.
169
170For `save-button` — no placeholders anywhere:
171
172```silo
173# Generated from :save-button "Save" .loading "Saving..." .done "Saved!" ;
174:record SaveButton :end
175
176:fn save-button ( -> SaveButton ) SaveButton :end
177
178:impl SaveButton
179  .message ( SaveButton -> Str ) drop "Save" ;
180  .loading ( SaveButton -> Str ) drop "Saving..." ;
181  .done    ( SaveButton -> Str ) drop "Saved!" ;
182:end
183```
184
185Usage:
186
187```silo
188save-button .message           # ⌊"Save"⌉
189save-button .loading           # ⌊"Saving..."⌉
190save-button .done              # ⌊"Saved!"⌉
191```
192
193For `login` — the main message takes `{app-name}`, the
194attributes don't take anything. The marker captures `app-name`
195so `.message` can use it; the other two methods ignore the
196captured value:
197
198```silo
199# Generated from :login {app-name} "Log in to {app-name}." .placeholder "..." .aria-label "..." ;
200:record Login .app-name Str :end
201
202:fn login ( Str -> Login ) Login :end
203
204:impl Login
205  .message ( Login -> Str )
206    pop-> l l .app-name "Log in to {}." format ;
207  .placeholder ( Login -> Str ) drop "user@example.com" ;
208  .aria-label  ( Login -> Str ) drop "Login email field" ;
209:end
210```
211
212```silo
213"Silo" login pop-> l
214l .message                     # ⌊"Log in to Silo."⌉
215l .placeholder                 # ⌊"user@example.com"⌉
216l .aria-label                  # ⌊"Login email field"⌉
217```
218
219The interesting case is when an attribute carries its own
220placeholders:
221
222```silo
223:update-available {version Str} "Update to version {version}."
224  .details {features Str} "New features: {features}"
225  .link    {url Str} "See the changelog at {url}" ;
226```
227
228Here each method's signature picks up only what it uses:
229
230```silo
231:record UpdateAvailable .version Str :end
232
233:fn update-available ( Str -> UpdateAvailable ) UpdateAvailable :end
234
235:impl UpdateAvailable
236  .message ( UpdateAvailable -> Str )
237    pop-> u u .version "Update to version {}." format ;
238  .details ( Str UpdateAvailable -> Str )
239    pop-> u pop-> features features "New features: {}" format ;
240  .link    ( Str UpdateAvailable -> Str )
241    pop-> u pop-> url url "See the changelog at {}" format ;
242:end
243```
244
245```silo
246"1.5" update-available pop-> u
247u .message                     # ⌊"Update to version 1.5."⌉
248"SIMD support" u .details      # ⌊"New features: SIMD support"⌉
249"https://silo.dev/log" u .link # ⌊"See the changelog at https://silo.dev/log"⌉
250```
251
252The main message's `{version}` is captured once; each
253attribute asks for its own extras independently. You never
254pre-compute data an attribute doesn't need.
255
256## Compile-time verification
257
258The compiler runs a pass over every `.sil` file in the project:
259
260- **No inventing messages.** A locale file can't introduce a
261  message that doesn't exist in the fallback — compile error.
262- **Placeholder parity.** Every placeholder in a locale's
263  translation must match the fallback by name and type.
264- **Plural coverage.** Each locale's required CLDR categories
265  must be present. Finnish needs `one` and `other`; Russian
266  needs `one`, `few`, and `many`; and so on.
267- **Attribute fallback.** Locale-specific attributes fall back
268  to the base catalogue when omitted; a locale doesn't have to
269  re-translate every attribute.
270
271The point of the pass is that you can ship a new version of your
272fallback catalogue — add a placeholder, rename a message, add an
273attribute — and every locale that hasn't been updated to match
274becomes a compile error. Schema drift is impossible.
275
276## Why `.sil`?
277
278Having the catalogue in its own format lets translators work in a
279file that doesn't require understanding Silo. The compiler meets
280them halfway: it checks that what they wrote fits the fallback's
281shape, and generates the code on their behalf. You don't pay any
282runtime cost for the abstraction — `greeting` is a plain word
283with a generated body, not a dictionary lookup.
284
285## Key points
286
287- `.sil` files sit next to `.si` sources. Fallback catalogue is
288  `<stem>.sil`; translations are `<stem>.<locale>.sil`.
289- The compiler generates a typed word per message. Placeholder
290  types drive the stack effect; attributes become record fields.
291- Typed placeholders: closed set `Str`, `Int`, `Float`,
292  `Decimal`, `Bool`, `Date`, `Time`, `DateTime`. Everything
293  else is a `Str`.
294- Selectors cover `Int` (CLDR plurals), `Str` (exact match), and
295  `Bool` (exhaustive). Wildcards via `_`.
296- `Locale` is an ICU4X locale value, built via
297  `(Locale .try-from)` from a BCP-47 tag. The
298  [`CurrentLocale` aspect](./11-aspects.simd) selects the active
299  one.
300- Compile-time verification: no inventing, placeholder parity,
301  plural coverage, attribute fallback. Schema drift in the
302  fallback is a compile error across every locale.
303
304Next: [temporal types](./15-temporal.simd) — dates, times,
305calendars, and how they use the locale aspect you just met.