Rea Language Specification — Part 4: Utilities (Sections 22–27)
Implementation status: Sections 22–27 describe advanced features (pluralization & localization, content protection, captions, escaping, comments, error handling) that are specified but not yet implemented in the proof-of-concept parser. The error handling philosophy (graceful degradation) is followed in the parser design. See REA-CHEATSHEET.md for detailed status.
22. Pluralization & Localization
Rea provides built-in functions for grammatically correct text across all languages. These replace any need for custom condition syntax by leveraging CLDR plural rules and standard internationalization APIs.
Pluralization with plural()
The plural() function maps a count to the correct grammatical form using CLDR plural categories. Categories vary by language — English has 2 (one, other), Slovak has 4 (one, few, many, other), Arabic has 6.
{plural(gold, zero="no coins", one="{} coin", other="{} coins")}For 0: "no coins", for 1: "1 coin", for 5: "5 coins". The {} placeholder inserts the count value.
Slovak (4 categories):
{plural(count, one="{} pero", few="{} perá", other="{} pier")}For 1: "1 pero", for 3: "3 perá", for 5: "5 pier".
CLDR plural categories:
| Category | English example | Used by |
|---|---|---|
zero | 0 items | Arabic, Latvian, Welsh |
one | 1 item | Most languages |
two | 2 items | Arabic, Hebrew, Slovenian |
few | 2-4 items | Czech, Slovak, Polish, Russian |
many | 5+ items | Polish, Russian, Arabic |
other | default | All languages (required fallback) |
The runtime resolves categories based on the story's language metadata. Authors only provide the categories their language requires — other is the mandatory fallback.
Text selection with select()
The select() function maps a string value to text variants. Use it for gender, pronoun, role-based, or any key-based text variation:
{select(pronoun, he="He draws his sword", she="She draws her sword", other="They draw their sword")}other is the fallback for unmatched values.
Role-based variation:
{select(reader.class, warrior="You swing your blade", mage="You cast a spell", other="You act")}Number formatting with format()
The format() function delegates to locale-aware number formatting (equivalent to Intl.NumberFormat):
Score: {format(player.score, style="decimal", grouping=true)}| Parameter | Values | Default |
|---|---|---|
style | decimal, percent, currency | decimal |
grouping | true, false | false |
min_fraction | integer (minimum decimal digits) | 0 |
max_fraction | integer (maximum decimal digits) | 3 |
currency | ISO 4217 code (e.g. EUR, USD) | story locale |
Price: {format(item.price, style="currency", currency="EUR")}
Chance: {format(hit_rate, style="percent")}
Distance: {format(meters, min_fraction=1, max_fraction=1)} mFantasy calendars with calendar()
The calendar() function maps real date components to custom names — perfect for fantasy world-building:
The month of {calendar(world.date, month="Frost,Bloom,Fire,Rain,Wind,Sun,Storm,Harvest,Mist,Shadow,Ice,Star")}For January: "Frost", for March: "Fire", for December: "Star".
| Parameter | Description |
|---|---|
month | Comma-separated list of 12 month names |
weekday | Comma-separated list of 7 day names (Monday = first) |
era | Expression defining era calculation |
Day of {calendar(world.date, weekday="Moonday,Fireday,Waterday,Earthday,Windday,Lightday,Darkday")},
{calendar(world.date, month="Frost,Bloom,Fire,Rain,Wind,Sun,Storm,Harvest,Mist,Shadow,Ice,Star")} the
{ordinal(world.date.day)}.Ordinal numbers with ordinal()
You finished in {ordinal(position)} place.Returns locale-appropriate ordinals: 1st, 2nd, 3rd in English; 1., 2., 3. in Slovak/German.
23. Content Protection (Lock)
The {lock} command protects story content, preventing readers from accessing chapters until conditions are met. This supports the platform's progressive download and monetization model.
Soft lock
Content is bundled but hidden until the reader solves a puzzle or meets a condition. The key is derived from the correct answer using PBKDF2 + AES-GCM:
{lock type="soft", key="a1b2c3d4e5f6g7h8i9j0" begin}
This chapter only unlocks when the reader provides the correct answer.
{end lock}Multiple valid answers:
{lock type="soft", key=["hash_answer_1", "hash_answer_2"] begin}
Either answer unlocks this content.
{end lock}How soft lock works internally:
- The author provides a plain-text answer during story creation
- The platform derives an AES-256-GCM key using PBKDF2 (SHA-256, 100k iterations) from the answer + random salt
- The locked content is encrypted with the derived key
- The salt and IV (12 bytes) are stored alongside the ciphertext
- When the reader submits an answer, the platform re-derives the key and attempts decryption
- AES-GCM's built-in authentication tag verifies the answer is correct (tamper-proof)
Hard lock
Content is stored on the server and downloaded only after the reader submits the correct key. This prevents extraction from the local package:
{lock type="hard", key="server_stored_hash" begin}
This chapter is downloaded only after correct verification.
{end lock}Hard locks use server-side validation: the reader's answer is hashed client-side and sent to the server, which compares it against the stored hash and returns the encrypted content only on match.
Conditional lock
Lock content behind story conditions:
{lock condition="player.level >= 10 and has_dragon_scale" begin}
The ancient text reveals itself only to the worthy.
{end lock}Encryption model
All content encryption in Rea uses the Web Crypto API for browser-safe, standards-compliant cryptography:
| Component | Algorithm / Standard |
|---|---|
| Encryption | AES-256-GCM (authenticated encryption) |
| Key derivation | PBKDF2 (SHA-256, 100k+ iterations) |
| IV | 12-byte random (per-block, never reused) |
| Auth tag | 128-bit (built into AES-GCM) |
| Key exchange | X25519 (cooperative readers, server-client) |
| Hashing | SHA-256 (checksums, answer verification) |
| Signing | Ed25519 (package signatures, author ID) |
The encryption model ensures:
- No plaintext in packages — locked content is always ciphertext in the
.reastfile - Forward secrecy — each lock block uses a unique IV; compromising one doesn't expose others
- Browser compatibility — all algorithms work in Chrome, Firefox, Safari, and Edge via
SubtleCrypto - Offline-capable — soft locks decrypt locally without server contact
24. Captions
The {caption} command adds descriptive captions to preceding content (images, code blocks, or text sections):
[!The ancient map < media/map.jpg]
{caption "A hand-drawn map found in the wizard's tower"}
{voice speaker="elena", emotion="sad" begin}
I never thought it would end this way.
{end voice}
{caption "Elena's final words"}25. Escaping & Raw Text
Escaping special characters
Use \ to escape any character with special meaning:
The price is \{not a command\}.
Use \_underscores\_ without italics.
The path was \*not\* what it seemed.Raw blocks
Content inside {raw begin} is rendered as-is with no processing:
{raw begin}
This {text} is *not* processed.
No _formatting_ or {commands} apply here.
{end raw}26. Comments
Author comments (hidden from reader)
{// This is a single-line comment}
{comment begin}
This is a multi-line comment.
Readers never see this.
{end comment}Single-line comments use {// text}. The // token causes the parser to ignore everything inside the braces — including any keywords like begin. For example, {// this has begin at the end begin} is still a valid single-line comment.
Multi-line comments use the {comment begin}...{end comment} block syntax, consistent with all other paired commands.
TODO markers (compile-time warnings)
{todo: Write the battle scene here}The platform/compiler shows these as warnings during development.
Notes (development annotations)
{note: This section needs playtesting with 3+ readers}27. Error Handling
Error categories
Rea distinguishes three categories of errors:
| Category | When detected | Examples |
|---|---|---|
| Parse error | Before execution | Syntax errors, unclosed blocks, malformed metadata, invalid nesting |
| Runtime error | During execution | Undefined variable, division by zero, missing divert target, recursion limit |
| Environment error | During execution | Missing media file, failed {include}, sensor unavailable, network failure |
Parse errors are always reported before the story runs (in strict mode) or silently patched (in graceful mode). Runtime and environment errors occur during story execution and are subject to graceful degradation.
Rea does not have try/catch. All error handling is implicit — the runtime silently recovers, and the reader's experience is never interrupted. Authors see errors during testing (strict mode); readers never do.
Graceful mode (default)
By default, Rea fails gracefully — the reader's experience is never broken:
| Error | Graceful behavior |
|---|---|
Undefined variable {gold} | Renders as empty string (nothing shown) |
Invalid expression {1 / 0} | Skipped silently |
| Missing image | Shows placeholder with alt text |
| Missing audio | Silently skipped, story continues |
| Missing video | Poster frame shown if available, else placeholder |
| Missing voice/TTS | Silently skipped |
Unclosed block {if begin} at EOF | Auto-closed at end of file |
Unknown command {magic} | Treated as print expression |
Failed {include "file.rea"} | Include silently ignored (as if file was empty) |
| Failed plugin command | Plugin output treated as empty |
| Sensor unavailable | world.has("sensor") returns false; see Section 21 |
Strict mode
Enable strict mode for development and testing:
{strict on}In strict mode, errors are shown as inline warnings visible only to the author (in a preview/development view). Readers never see error messages — they always get graceful behavior.
Example strict mode warnings:
⚠ Line 42: Undefined variable "goldd" — did you mean "gold"?
⚠ Line 87: Unclosed block command {if begin} opened at line 83
⚠ Line 15: Missing media "media/castle.jpg" — file not found in package
⚠ Line 63: Expression error in {1 / 0} — division by zero
⚠ Line 10: Include "lib/missing.rea" — file not found in package
⚠ Line 25: Plugin "compass" failed — function "bearing" is not definedStrict mode features:
- Typo detection with suggestions (Levenshtein distance on variable/command names)
- Line numbers in all warnings
- Highlights unclosed blocks with the opening line reference
- Validates media references against the package manifest
- Validates
{include}paths against the package structure
Fallback values
Where it makes sense, syntax supports optional inline fallback values:
[!map < media/map.png fallback="media/map-lowres.png"]
[?thunder < sounds/thunder.mp3 fallback="sounds/rain.mp3"]If the primary resource fails, the fallback is used. If the fallback also fails, the platform applies its default graceful behavior (placeholder for images, silence for audio, etc.).
External API access
External API calls (network requests from within a story) must be declared in the reast.json manifest via allowed_urls. URLs must not appear anywhere in .rea text — authors reference APIs by alias only. This ensures all external access is declared, auditable, and permission-controlled.
{
"title": "Weather Story",
"allowed_urls": [
{
"alias": "weather",
"url": "https://api.weather.example.com",
"params": ["lat", "lng"]
},
{ "alias": "maps", "url": "https://maps.example.com" }
]
}Each entry in allowed_urls is an object with:
| Field | Type | Description |
|---|---|---|
alias | string | Short name used to reference this API in .rea |
url | string | Base URL prefix the story may access |
params | string[] | Optional list of allowed query parameter names |
Authors reference allowed APIs by alias in story code. If a request fails, the runtime returns undefined and the story continues.