Skip to content

I18n (Localization)

This module provides a small, typed localization surface that fits AIVI's existing primitives:

  • Effect E A for 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

aivi
use aivi.i18n

Types

aivi
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:

  • Key and Message are implemented as record-shaped sigil values. They are open records in practice (tooling may attach extra fields).
  • Bundle.entries is keyed by the key text (Text). Key is a typed wrapper for that text.

Sigils

~k"..." (keys)

aivi
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)

aivi
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
// AIVI Text literal that contains a message template
raw = "Hello, \\{name:Text\\}!"
msg = message raw

API

aivi
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 -> {} -> Text

Properties catalogs

bundleFromProperties consumes a simple .properties-style format:

text
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:

text
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 -> Text
  • bundle (a Map of 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:

  1. read the conventional environment variables (LC_ALL, LC_MESSAGES, LANG) via aivi.system.env
  2. parse with parseLocale
  3. fall back to a known default when parsing fails
aivi
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:

  • parseLocale accepts - 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:

aivi
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:

aivi
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)

aivi
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):

  • DateTime placeholders are currently rendered using the runtime's default DateTime -> Text formatting (locale-neutral; typically ISO-8601-ish).
  • If you need locale-specific date formatting today, pre-format the date into a Text value 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):

aivi
use aivi.i18n

// Prefer `userBundle`, fall back to `defaultBundle`.
msg = tWithFallback [userBundle, defaultBundle] (~k"app.welcome") { name: "Alice" }