Linguini has two file types:
.lgs— schema. Defines message signatures, enums, and type aliases..lgl— locale. Implements messages for one locale.
Declares a set of named variants. Names are PascalCase. Variants are lowercase.
enum Fruit { apple, pear, orange }
enum Size { small, big }
Built-in primitive types are String, Number, Decimal, Date, and
Boolean. They are represented internally by TypeKind, so parser, analyzer,
IR, and codegen share one canonical type list.
Attach a formatter to a primitive type to make formatting part of the schema contract. Locale files can then interpolate the value directly; generated code applies the schema formatter automatically.
type Money = Decimal @currency(code = "EUR")
type ShortDate = Date @date(style = "short")
checkout_total(amount: Money, created: ShortDate)
checkout_total = Total {amount} on {created}
Locale authors can still override formatting at the interpolation site:
checkout_total = Total {amount @number} on {created @date(style = "long")}
Primitive Number, Decimal, and Date parameters also get schema-owned
defaults even without aliases:
summary(count: Number, total: Decimal, created: Date)
summary = {count} items, {total}, {created}
Generated TypeScript emits the locale CLDR data once per locale module and
passes a shared FORMATTER_DATA constant into each formatter call, so repeated
interpolations do not inline the full number/date/currency table.
The formatter list is canonical:
| Formatter | Applies to |
|---|---|
@number |
Number, Decimal |
@currency |
Number, Decimal alias |
@date |
Date |
A message is a named entry the app can call. Parameters are typed.
delivery(fruit: Fruit, size: Size, count: Number)
greeting(name: String)
Doc comments attach to the next declaration and appear in codegen output and LSP hover.
/// Shown on the delivery confirmation card.
delivery(fruit: Fruit, size: Size, count: Number)
A bare identifier inside a namespace block is a message with no parameters.
email_input {
label
placeholder
aria
}
A block groups related messages under a shared prefix.
auth {
sign_in(email: String)
sign_out
error_invalid_credentials
}
A project can contain multiple .lgs files under the configured schema directory.
Declarations from those files are loaded into one project-level schema namespace.
Source-level import and export syntax is not part of the current language.
A locale file implements all messages declared in the corresponding schema.
greeting = Hello, {name}!
sign_out = Sign out
Reference a parameter by name inside { }:
greeting = Hello, {name}!
price = Total: {amount} {currency}
Use let in locale files for reusable local text fragments. Local variables are
not part of the schema API and can be referenced from messages, forms, and
functions in the same locale module.
let cart_label = Cart
cart_summary = {cart_label}: {count} {item.nom(count)}
Plural is always available with variants one | few | many | other.
Any Number passed where Plural is expected converts automatically
using CLDR plural rules for the active locale. No wrapper function needed.
impl declares the linguistic implementation of an enum. Each block corresponds
to one variant and may contain typed fields, form declarations, and static
string blocks.
enum Gender { masculine, feminine, neuter, other }
impl Fruit {
apple {
Gender = neuter
emoji = 🍎
form nom(Plural) {
one => яблоко
few => яблока
_ => яблок
}
form gen(Plural) {
one => яблока
_ => яблок
}
display {
short = ябл.
long = спелое яблоко
}
}
pear {
Gender = feminine
form nom(Plural) {
one => груша
few => груши
_ => груш
}
}
}
Typed fields use TypeName = value. The type is inferred from the
PascalCase key — no ambiguity with lowercase string fields.
Gender = neuter // typed: Gender enum
emoji = 🍎 // plain string field
Accessing fields and forms in templates:
fruit.Gender // typed field access
fruit.nom(count) // form call — count auto-converts to Plural
form matches on enum variants and returns a string. It cannot take String
parameters — use fn for that.
Parameters are types only, no names. Each nesting level dispatches on one
parameter top-down. _ matches all remaining variants at the current level.
form Delivered(Plural, Gender) {
one {
masculine => Доставлен
feminine => Доставлена
neuter => Доставлено
_ => Доставлено
}
_ => Доставлены
}
Multiple parameters, deeper nesting:
form SizeAdj(Size, Plural, Gender) {
small {
one {
masculine => маленький
feminine => маленькая
neuter => маленькое
_ => маленький
}
_ => маленьких
}
big {
one {
masculine => большой
feminine => большая
neuter => большое
_ => большой
}
_ => больших
}
}
Ordering: parameters should go from the enum with fewest variants to the most.
This keeps the top levels of the dispatch tree narrow (param_order lint).
Exhaustiveness: all match expressions must be exhaustive. Cover every variant
explicitly or include _. A non-exhaustive form is a compile-time error.
fn works like form but can accept named String parameters and interpolate
them into output values.
fn delivery_note(item: String, Plural, Gender) {
one {
feminine => Доставлена {item}
_ => Доставлен {item}
}
_ => Доставлены {item}
}
Use form when output depends only on grammatical categories.
Use fn when output embeds a dynamic string value.
A fn can be written inline as an interpolation value:
greeting = Привет, {fn(Gender) { masculine => дорогой, feminine => дорогая, _ => дорогой }} {name}!
_ matches all remaining variants of the current parameter. Must be last.
form F(Gender) {
feminine => она
_ => он
}
- Non-exhaustive match without
_ - Type mismatch at call site
- Unresolved reference in interpolation
- Missing message implementation in locale
implvariant not declared in its enum- Unknown formatter annotation
- Unused message — in schema, never referenced in source
fnwith noStringparameters — suggestform
Lints are reported by linguini check. Inline lint suppression syntax is not part of the current language.
| Lint | Description |
|---|---|
param_order |
Order form params from fewest variants to most |
unreachable_arm |
Arm after _ at the same level |
collapsible_arms |
Multiple arms with identical output |
fn_without_strings |
fn with no String params — use form |
incomplete_impl |
impl missing variants declared in enum |
redundant_wildcard |
All variants covered, _ is unreachable |
[project]
name = "my-app"
default_locale = "en"
locales = ["en", "ru"]
[paths]
schema = "linguini/schema"
locale = "linguini/locale"
[targets.ts]
out = "src/generated/linguini"
module = "esm" # esm | cjs
declaration = true
gitignore = true # emit generated .gitignore