I18n (Localization)
This module provides a small, typed localization surface that fits AIVI's existing primitives:
Effect E Afor explicit failure/cancellation.- Sigils for compile-time validation of structured literals.
This is intentionally a minimal v0.1 foundation: key validation, message template validation, bundle loading, lookup, and rendering.
Module
use aivi.i18nTypes
type Locale = { language: Text, region: Option Text, variants: List Text, tag: Text }
type Key = { tag: Text, body: Text, flags: Text }
type Message = { tag: Text, body: Text, flags: Text }
type Bundle = { locale: Locale, entries: Map Text Message }Notes:
KeyandMessageare implemented as record-shaped sigil values. They are open records in practice (tooling may attach extra fields).Bundle.entriesis keyed by the key text (Text).Keyis a typed wrapper for that text.
Sigils
~k"..." (keys)
welcomeKey : Key
welcomeKey = ~k"app.welcome"~k is validated at parse time:
- non-empty
- dot-separated segments (
a.b.c) - no empty segments (
"a..b",".a","a."are rejected) - each segment must start with
[A-Za-z_]and contain only[A-Za-z0-9_-]
~m"..." (messages)
welcomeMsg : Message
welcomeMsg = ~m"Hello, {name:Text}!"~m is validated at parse time using a small template language:
- literal text
- escaped braces:
{{renders{,}}renders} - placeholders:
{name}or{name:Type}
Supported placeholder types in v0.1:
Text,Int,Float,Bool,Decimal,DateTime
When rendering, a placeholder with a :Type annotation is checked at runtime.
Important: AIVI Text literals also support interpolation with { Expr } (see Syntax). If you want literal braces inside a Text literal, escape them as \\{ and \\}:
// AIVI Text literal that contains a message template
raw = "Hello, \\{name:Text\\}!"
msg = message rawAPI
parseLocale : Text -> Result Text Locale
key : Text -> Result Text Key
message : Text -> Result Text Message
render : Message -> {} -> Result Text Text
bundleFromProperties : Locale -> Text -> Result Text Bundle
bundleFromPropertiesFile : Locale -> Text -> Effect Text (Result Text Bundle)
tResult : Bundle -> Key -> {} -> Result Text Text
tOpt : Bundle -> Key -> {} -> Option Text
t : Bundle -> Key -> {} -> Text
tWithFallback : List Bundle -> Key -> {} -> TextProperties catalogs
bundleFromProperties consumes a simple .properties-style format:
app.welcome = Hello, {name:Text}!
app.cartItems = You have {count:Int} items.Lines starting with # and blank lines are ignored.
Tooling: aivi i18n gen
The compiler ships a small generator for .properties catalogs:
aivi i18n gen <catalog.properties> --locale <tag> --module <name> --out <file>It emits an AIVI module containing:
KeyId(a generated sum type of keys)keyText : KeyId -> Textbundle(aMapof compiled~m"..."messages)t : KeyId -> {} -> Text
This lets projects use generated constructors (typo-proof) while keeping runtime lookup on Text keys.
Common Patterns
1) Determine the user's locale (system best-effort)
v0.1 does not (yet) expose a dedicated system.locale API. The recommended approach is:
- read the conventional environment variables (
LC_ALL,LC_MESSAGES,LANG) viaaivi.system.env - parse with
parseLocale - fall back to a known default when parsing fails
use aivi.i18n
use aivi.system (env)
use aivi.text (split)
defaultLocale : Locale
defaultLocale = { language: "en", region: Some "US", variants: [], tag: "en-US" }
systemLocale : Effect Text Locale
systemLocale = effect {
lcAll <- env.get "LC_ALL"
lcMsg <- env.get "LC_MESSAGES"
lang <- env.get "LANG"
tagOpt =
lcAll ?
| Some t => Some t
| None =>
lcMsg ?
| Some t => Some t
| None => lang
tag =
tagOpt ?
| Some t => t
| None => defaultLocale.tag
// Many systems report locale tags like `en_US.UTF-8` or `de_DE@euro`.
baseTag =
(split "." tag) ?
| [] => tag
| [x, ..._] => x
cleanTag =
(split "@" baseTag) ?
| [] => baseTag
| [x, ..._] => x
parseLocale cleanTag ?
| Ok loc => pure loc
| Err _ => pure defaultLocale
}Notes:
parseLocaleaccepts-or_separated tags (e.g.en-US,en_US).
2) Load a bundle from disk (with a Resource)
If you just need a file-based catalog, use bundleFromPropertiesFile:
use aivi.i18n
loadBundle : Locale -> Text -> Effect Text Bundle
loadBundle locale path = effect {
res <- bundleFromPropertiesFile locale path
res ?
| Ok b => pure b
| Err e => fail e
}If you want to be explicit about resource lifetimes (or reuse an already-open handle), combine aivi.file.open with bundleFromProperties:
use aivi.file as file
use aivi.i18n
loadBundleWithResource : Locale -> Text -> Effect Text Bundle
loadBundleWithResource locale path = effect {
h <- file.open path
txtRes <- file.readAll h
txtRes ?
| Ok txt =>
bundleFromProperties locale txt ?
| Ok b => pure b
| Err e => fail e
| Err e => fail e
}3) Translate a message with placeholders (including dates)
use aivi.calendar (DateTime)
use aivi.i18n
props = "app.welcome = Hello, \\{name:Text\\}!\\napp.lastLogin = Last login: \\{when:DateTime\\}.\\n"
main = effect {
locale =
parseLocale "en-US"
or { language: "en", region: Some "US", variants: [], tag: "en-US" }
bundle =
bundleFromProperties locale props
or { locale: locale, entries: Map.empty }
print (t bundle (~k"app.welcome") { name: "Alice" })
print (t bundle (~k"app.lastLogin") { when: ~dt(2026-02-12T15:30:00Z) })
}Formatting note (v0.1):
DateTimeplaceholders are currently rendered using the runtime's defaultDateTime -> Textformatting (locale-neutral; typically ISO-8601-ish).- If you need locale-specific date formatting today, pre-format the date into a
Textvalue at the boundary (host runtime / external source), and use{when:Text}in your message template.
4) Bundles and fallbacks
Use tWithFallback when you have multiple bundles (e.g. app bundle + shared bundle, or user locale + default locale):
use aivi.i18n
// Prefer `userBundle`, fall back to `defaultBundle`.
msg = tWithFallback [userBundle, defaultBundle] (~k"app.welcome") { name: "Alice" }