cli/docs/plans/2026-01-30-semantic-i18n-design.md
Snider a987980e82 docs(i18n): finalize API naming for semantic i18n
Stable API (permanent, cannot change):
- _() - simple gettext-style lookup
- T() / C() - compose (semantic intent resolution)
- S() / Subject() - typed subject with metadata

Renamed Transmute() to Compose() for clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 12:00:53 +00:00

10 KiB

Semantic i18n System Design

Overview

Extend the i18n system beyond simple key-value translation to support semantic intents that encode meaning, enabling:

  • Composite translations from reusable fragments
  • Grammatical awareness (gender, plurality, formality)
  • CLI prompt integration with localized options
  • Reduced calling code complexity

Goals

  1. Simple cases stay simple - _("key") works as expected
  2. Complex cases become declarative - Intent drives output, not caller logic
  3. Translators have power - Grammar rules live in translations, not code
  4. CLI integration - Questions, confirmations, choices are first-class

API Design

Function Reference (Stable API)

These function names are permanent - choose carefully, they cannot change.

Function Alias Purpose
_() - Simple gettext-style lookup
T() C() Compose - semantic intent resolution
S() Subject() Create typed subject with metadata

Simple Translation: _()

Standard gettext-style lookup. No magic, just key → value.

i18n._("cli.success")                    // "Success"
i18n._("common.label.error")             // "Error:"
i18n._("common.error.failed", map[string]any{"Action": "load"})  // "Failed to load"

Compose: T() / C()

Semantic intent resolution. Takes an intent key from core.* namespace and returns a Composed result with multiple output forms.

// Full form
result := i18n.T("core.delete", i18n.S("file", path))
result := i18n.C("core.delete", i18n.S("file", path))  // Alias

// Result contains all forms
result.Question  // "Delete /path/to/file.txt?"
result.Confirm   // "Really delete /path/to/file.txt?"
result.Success   // "File deleted"
result.Failure   // "Failed to delete file"
result.Meta      // IntentMeta{Dangerous: true, Default: "no", ...}

Subject: S() / Subject()

Creates a typed subject with optional metadata for grammar rules.

// Simple
i18n.S("file", "/path/to/file.txt")

// With count (plurality)
i18n.S("commit", commits).Count(len(commits))

// With gender (for gendered languages)
i18n.S("user", name).Gender("female")

// Chained
i18n.S("file", path).Count(3).In("/project")

Type Signatures

// Simple lookup
func _(key string, args ...any) string

// Compose (T and C are aliases)
func T(intent string, subject *Subject) *Composed
func C(intent string, subject *Subject) *Composed

// Subject builder
func S(noun string, value any) *Subject
func Subject(noun string, value any) *Subject

// Composed result
type Composed struct {
    Question string
    Confirm  string
    Success  string
    Failure  string
    Meta     IntentMeta
}

// Subject with metadata
type Subject struct {
    Noun   string
    Value  any
    count  int
    gender string
    // ... other metadata
}

func (s *Subject) Count(n int) *Subject
func (s *Subject) Gender(g string) *Subject
func (s *Subject) In(location string) *Subject

// Intent metadata
type IntentMeta struct {
    Type      string   // "action", "question", "info"
    Verb      string   // Reference to common.verb.*
    Dangerous bool     // Requires confirmation
    Default   string   // "yes" or "no"
    Supports  []string // Extra options like "all", "skip"
}

CLI Integration

The CLI package uses T() internally for prompts:

// Confirm uses T() internally
confirmed := cli.Confirm("core.delete", i18n.S("file", path))
// Internally: result := i18n.T("core.delete", subject)
// Displays: result.Question + localized [y/N]
// Returns: bool

// Question with options
choice := cli.Question("core.save", i18n.S("changes", 3).Count(3), cli.Options{
    Default: "yes",
    Extra:   []string{"all"},
})
// Displays: "Save 3 changes? [a/y/N]"
// Returns: "yes" | "no" | "all"

// Choice from list
selected := cli.Choose("core.select.branch", branches)
// Displays localized prompt with arrow selection

cli.Confirm()

func Confirm(intent string, subject *i18n.Subject, opts ...ConfirmOption) bool

// Options
cli.DefaultYes()     // Default to yes instead of no
cli.DefaultNo()      // Explicit default no
cli.Required()       // No default, must choose
cli.Timeout(30*time.Second)  // Auto-select default after timeout

cli.Question()

func Question(intent string, subject *i18n.Subject, opts ...QuestionOption) string

// Options
cli.Extra("all", "skip")    // Extra options beyond y/n
cli.Default("yes")          // Which option is default
cli.Validate(func(s string) bool)  // Custom validation

cli.Choose()

func Choose[T any](intent string, items []T, opts ...ChooseOption) T

// Options
cli.Display(func(T) string)  // How to display each item
cli.Filter()                 // Enable fuzzy filtering
cli.Multi()                  // Allow multiple selection

Reserved Namespaces

common.* - Reusable Fragments

Atomic translation units that can be composed:

{
  "common": {
    "verb": {
      "edit": "edit",
      "delete": "delete",
      "create": "create",
      "save": "save",
      "update": "update",
      "commit": "commit"
    },
    "noun": {
      "file": { "one": "file", "other": "files" },
      "commit": { "one": "commit", "other": "commits" },
      "change": { "one": "change", "other": "changes" }
    },
    "article": {
      "the": "the",
      "a": { "one": "a", "vowel": "an" }
    },
    "prompt": {
      "yes": "y",
      "no": "n",
      "all": "a",
      "skip": "s",
      "quit": "q"
    }
  }
}

core.* - Semantic Intents

Intents encode meaning and behavior:

{
  "core": {
    "edit": {
      "_meta": {
        "type": "action",
        "verb": "common.verb.edit",
        "dangerous": false
      },
      "question": "Should I {{.Verb}} {{.Subject}}?",
      "confirm": "{{.Verb | title}} {{.Subject}}?",
      "success": "{{.Subject | title}} {{.Verb | past}}",
      "failure": "Failed to {{.Verb}} {{.Subject}}"
    },
    "delete": {
      "_meta": {
        "type": "action",
        "verb": "common.verb.delete",
        "dangerous": true,
        "default": "no"
      },
      "question": "Delete {{.Subject}}? This cannot be undone.",
      "confirm": "Really delete {{.Subject}}?",
      "success": "{{.Subject | title}} deleted",
      "failure": "Failed to delete {{.Subject}}"
    },
    "save": {
      "_meta": {
        "type": "action",
        "verb": "common.verb.save",
        "supports": ["all", "skip"]
      },
      "question": "Save {{.Subject}}?",
      "success": "{{.Subject | title}} saved"
    },
    "commit": {
      "_meta": {
        "type": "action",
        "verb": "common.verb.commit",
        "dangerous": false
      },
      "question": "Commit {{.Subject}}?",
      "success": "{{.Subject | title}} committed",
      "failure": "Failed to commit {{.Subject}}"
    }
  }
}

Template Functions

Available in translation templates:

Function Description Example
title Title case {{.Name | title}} → "Hello World"
lower Lower case {{.Name | lower}} → "hello world"
upper Upper case {{.Name | upper}} → "HELLO WORLD"
past Past tense verb {{.Verb | past}} → "edited"
plural Pluralize noun {{.Noun | plural .Count}} → "files"
article Add article {{.Noun | article}} → "a file"
quote Wrap in quotes {{.Path | quote}}"/path/to/file"

Implementation Plan

Phase 1: Foundation

  1. Define Composed and Subject types
  2. Add S() / Subject() builder
  3. Add T() / C() with intent resolution
  4. Parse _meta from JSON
  5. Add template functions (title, lower, past, etc.)

Phase 2: CLI Integration

  1. Implement cli.Confirm() using intents
  2. Implement cli.Question() with options
  3. Implement cli.Choose() for lists
  4. Localize prompt characters [y/N] → [j/N] etc.

Phase 3: Grammar Engine

  1. Verb conjugation (past tense, etc.)
  2. Noun plurality with irregular forms
  3. Article selection (a/an, gender)
  4. Language-specific rules

Phase 4: Extended Languages

  1. Gender agreement (French, German, etc.)
  2. Formality levels (Japanese, Korean, etc.)
  3. Right-to-left support
  4. Plural forms beyond one/other (Russian, Arabic, etc.)

Example: Full Flow

// In cmd/dev/dev_commit.go
path := "/Users/dev/project"
files := []string{"main.go", "config.yaml"}

// Old way (hardcoded English, manual prompt handling)
fmt.Printf("Commit %d files in %s? [y/N] ", len(files), path)
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" {
    return
}

// New way (semantic, localized, integrated)
if !cli.Confirm("core.commit", i18n.S("file", path).Count(len(files))) {
    return
}

// For German user, displays:
// "2 Dateien in /Users/dev/project committen? [j/N]"
// (note: "j" for "ja" instead of "y" for "yes")

JSON Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "common": {
      "description": "Reusable translation fragments",
      "type": "object"
    },
    "core": {
      "description": "Semantic intents with metadata",
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "_meta": {
            "type": "object",
            "properties": {
              "type": { "enum": ["action", "question", "info"] },
              "verb": { "type": "string" },
              "dangerous": { "type": "boolean" },
              "default": { "enum": ["yes", "no"] },
              "supports": { "type": "array", "items": { "type": "string" } }
            }
          },
          "question": { "type": "string" },
          "confirm": { "type": "string" },
          "success": { "type": "string" },
          "failure": { "type": "string" }
        }
      }
    }
  }
}

Open Questions

  1. Verb conjugation library - Use existing Go library or build custom?
  2. Gender detection - How to infer gender for subjects in gendered languages?
  3. Fallback behavior - What happens when intent metadata is missing?
  4. Caching - Should compiled templates be cached?
  5. Validation - How to validate intent definitions at build time?