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.