runtime/prompts/<lang>/<role>.j2 (MiniJinja, 26 ruoli),
runtime/i18n.sqlite (118 chiavi) e i manifest TOML degli executor con tabella
[description].<lang> + file companion manifest.lang_state.json.
multilang — tre layer, source-of-truth latest-wins, add-languageQuesto documento definisce come Metnos parla più lingue: dove vivono le stringhe, chi può modificarle, come si propaga un cambio da una lingua all'altra, come si aggiunge una nuova lingua. Italiano e inglese sono entrambi presenti dal primo giorno; da settembre 2026 il sistema deve poter accogliere una terza lingua (francese, spagnolo, tedesco) senza toccare il codice.
Il documento copre:
metnos-prompts add-language <code> e i suoi effetti;METNOS_LANG di processo (boot resolve) e source-of-truth (latest-wins, dinamico);Non copre, e demanda altrove:
runtime/i18n.py;Ogni stringa che Metnos pronuncia, o che un LLM legge come parte di una sua istruzione, vive in uno e un solo dei tre layer seguenti. La separazione è per natura della stringa, non per comodità: nessuna stringa abita due layer.
| Layer | Cosa contiene | Storage | Volume |
|---|---|---|---|
| Layer 1 — prompt LLM | Il testo che istruisce il pianificatore, il vaglio, il describe, il classify, il synt 5-stage. Template MiniJinja con variabili ({{ tools }}, {{ history }}). |
runtime/prompts/<lang>/<role>.j2 |
26 ruoli × N lingue |
| Layer 2 — manifest description | Description e affinity keywords degli executor. Sono il prompt del tool per il pianificatore (CLAUDE.md §2.5). | Tabella [description].<lang> dentro al manifest TOML; companion file manifest.lang_state.json per version_hash + source_hash per lingua. |
~52 executor × N lingue |
| Layer 3 — messaggi user-facing | ERR_*, WARN_*, MSG_*, LOG_* (errori, warning, conferme, notifiche al canale). Description executor migrate qui in lettura runtime. | SQLite ~/.local/share/metnos/i18n.sqlite; tabella i18n(key, lang, text, needs_translation, source_lang, version_hash). |
118 chiavi standard + 79 description = 197 entry × N lingue |
La directory runtime/prompts/ ha un sotto-albero per lingua.
Ogni ruolo (planner, vaglio_judge,
describe_entries, …) è un file .j2 con
stessa struttura in ogni lingua: stesse variabili, stessa
intelaiatura di sezioni, solo testo localizzato.
runtime/prompts/
it/
planner.j2
vaglio_judge.j2
describe_entries.j2
… (26 file)
en/
planner.j2 # stessa intelaiatura, testo in EN
…
_pending/
planner.j2.candidate # candidato in attesa di review
fr/ # opzionale, dopo metnos-prompts add-language fr
…
Il loader runtime/prompt_loader.py è chiamato da ogni componente
runtime con prompt_loader.get(role, lang, **vars). Lingua risolta
come os.environ["METNOS_LANG"], fallback config.DEFAULT_LANG
(oggi "it"). Se manca il file della lingua chiesta, il loader fa
fallback alla lingua sorgente (di norma it) e logga un evento
prompt.lang_fallback.
Il manifest TOML di un executor (executors/<name>/manifest.toml)
ha la sezione [description] a tabella per lingua, e un companion
JSON manifest.lang_state.json nel medesimo folder per tenere
traccia di chi e' source e quando.
# manifest.toml [description] it = """ Cerca file su filesystem per pattern e finestra temporale. … """ en = """ Find files on the filesystem by pattern and time window. … """ [affinity] it = ["cerca", "trova", "file", "ricerca", "…"] en = ["find", "search", "files", "lookup", "…"]
// manifest.lang_state.json (companion file)
{
"lang_state": {
"it": {
"version_hash": "a3f9...",
"source_hash": null, // it = source corrente
"source_lang": null,
"translated_by": null,
"reviewed_at": "2026-04-22T10:30Z"
},
"en": {
"version_hash": "7c1b...",
"source_hash": "a3f9...", // EN derivato da hash IT a3f9
"source_lang": "it",
"translated_by": "wise:gemma4-26b",
"reviewed_at": "2026-04-22T11:05Z"
}
}
}
A run time il pianificatore legge la description nella lingua corrente
(METNOS_LANG) via loader.describe(name, lang); se la
lingua manca, fallback IT con log evento.
Il modulo runtime/i18n.py espone i18n.get(key, lang=None, **vars)
con interpolazione stile {name}. Le chiavi seguono la convenzione
SCOPE_DOMAIN_DETAIL: ERR_FILE_NOT_FOUND,
WARN_CAP_REACHED, MSG_APPROVAL_GRANTED,
LOG_TURN_ENDED.
Le description degli executor sono tutte migrate nel DB con prefisso
EX_DESCRIPTION_<name> e EX_AFFINITY_<name>;
il loader del manifest le legge dal DB invece che dal TOML. Il TOML resta
autoritativo per i nuovi executor; il DB le importa al primo load.
Nessuna lingua e' canonica per costruzione. Chi e' stato editato per ultimo (in qualunque lingua) diventa source per le altre.
Per ogni risorsa multilingua — un prompt, una description, una chiave i18n — teniamo due hash:
version_hash — SHA-256 del testo corrente nella sua lingua;source_hash — version_hash della lingua sorgente al momento dell'ultimo allineamento. null se la lingua è essa stessa la sorgente corrente.
Quando un editor (umano o batch) cambia il testo della lingua L1
di una risorsa, il version_hash di L1 cambia. Questo invalida
implicitamente tutte le altre lingue L2, L3, … che hanno
source_hash uguale al vecchio version_hash di
L1: non sono più sincronizzate. Il daemon notturno
i18n_translator.run_loop() le ritrova come pending e
genera un nuovo candidato in _pending/.
| Editato | Effetto immediato | Notte successiva |
|---|---|---|
prompts/it/planner.j2 |
version_hash(it) nuovo. source_hash(en) ora obsoleto. |
Il translator rigenera prompts/en/_pending/planner.j2.candidate a partire dal nuovo IT. metnos-prompts review planner --lang=en mostra il diff. |
prompts/en/planner.j2 (modifica diretta in EN) |
version_hash(en) nuovo. EN diventa source corrente: source_lang=en per IT. |
Il translator considera ora EN come sorgente. Se source_hash(it) e' obsoleto, rigenera prompts/it/_pending/planner.j2.candidate. |
i18n.set("MSG_APPROVAL_GRANTED", "fr", "…") |
Solo la riga fr di quella chiave cambia. |
Nessun trigger sulle altre lingue: la modifica e' solo locale a fr. |
Il confronto version_hash vs source_hash è
deterministico (CLAUDE.md §7.9). L'LLM interviene solo per
generare il candidato di traduzione; il quando rigenerare e
il quale e' la sorgente sono codice puro, niente prompt.
Il flusso completo per aggiungere una nuova lingua è un comando solo:
$ metnos-prompts add-language fr Lingua 'fr' bootstrap completato. - prompts/fr/ creata (vuota, daemon notturno generera' candidate) - i18n.sqlite: fr bootstrap rows pending - Manifest description: il daemon scansionera' al prossimo cycle Per triggerare manualmente la traduzione subito: /opt/myclaw/deploy/run_prompts_translator.sh Per attivare la lingua: METNOS_LANG=fr nei systemd unit + restart
Quattro effetti, tutti idempotenti:
runtime/prompts/fr/ vuota. Il daemon notturno la riempirà con candidati a partire dalla lingua sorgente (default it; specificabile con --source-lang en se EN e' diventata la lingua piu' aggiornata).[description].fr mancante e genera candidati nel manifest.lang_state.json (campo fr) marcato needs_review=true.i18n_cli.add-lang fr popola la tabella i18n con un placeholder per ogni chiave esistente, needs_translation=1, source_lang=it. Il daemon traduce a batch.~/.local/share/metnos/multilang/audit.jsonl con timestamp, codice ISO, source_lang scelta.
La lingua resta inattiva finché non si setta
METNOS_LANG=fr nei systemd unit del runtime e si riavvia il
daemon. Questo evita che frammenti tradotti a metà appaiano in
produzione mentre il review umano e' ancora in corso.
_pending/, non promuove.
metnos-prompts review <role> --lang=fr mostra il diff;
metnos-prompts mark-synced <role> --lang=fr promuove il
candidato a runtime, ma solo se la validation passa
(placeholder match, sintassi MiniJinja, len ratio entro [0.6, 2.0]).
METNOS_LANG di processo vs source-of-truth dinamico
METNOS_LANG e' la lingua di lettura del processo:
quale lingua il pianificatore vede come prompt, quale lingua compare nei
messaggi all'utente. E' una variabile statica, risolta al boot dei systemd
unit, costante per tutta la vita del processo.
Il source-of-truth (latest-wins di §3) e' un attributo
dinamico per risorsa: per la risorsa A può essere IT, per la B
EN, per la C FR — dipende da chi ha editato per ultimo. Il daemon
translator usa il source-of-truth per decidere da-cosa-tradurre-cosa,
indipendentemente da METNOS_LANG.
| Aspetto | METNOS_LANG | Source-of-truth latest-wins |
|---|---|---|
| Granularità | Globale al processo | Per risorsa (prompt / description / chiave) |
| Quando si risolve | Al boot del systemd unit | A ogni edit, a ogni cycle del daemon |
| Effetto | Lingua mostrata all'utente / data al LLM | Lingua da cui rigenerare i candidati |
| Modifica | Edit unit + restart | Automatica, ogni edit triggera ricalcolo hash |
Capita che un editor preferisca scrivere una nuova versione di un prompt
direttamente nella lingua secondaria (es. EN), per poi voler riportare la
modifica in IT. Il latest-wins lo fa automaticamente: quando l'edit EN va
in commit, il version_hash(en) diventa nuovo, EN diventa
source corrente, e la notte successiva il candidato IT viene
rigenerato in prompts/it/_pending/<role>.j2.candidate.
Lo stesso comando di review/mark-synced funziona simmetrico:
metnos-prompts review planner --lang=it mostra il diff,
mark-synced planner --lang=it promuove. La direzione di traduzione
e' codificata nel source_lang del companion JSON, non nel verso
del comando.
retro-translate separato: la simmetria del meccanismo lo rende
implicito. Il vincolo è non editare entrambe le lingue contemporaneamente
con cambi divergenti: l'ultima ad essere salvata vince e l'altra perde
(con log multilang.simultaneous_edit_warning).
Oggi i template Jinja2 di channels/templates/ (carta di
approval, dialog form, summary di turno) sono solo IT. Il layer 4 sarà
la replica della disciplina layer 1 con sotto-albero
channels/templates/<lang>/ e identica meccanica
latest-wins. Stima: 8-10 template, 2-3 ore di porting una volta che
arriva la terza lingua.
Il flag --quality {wise,frontier} di
metnos-prompts translate e translate-all permette
di scegliere il tier LLM. wise e' il default (Gemma 4 26B
locale, gratuito); frontier usa Anthropic Opus 4.7 con costo
~$0.015/call. La differenza emerge sui prompt lunghi e sui ruoli (es.
vaglio_judge) dove la sfumatura semantica e' alta. Documentato
nel man del CLI come opt-in cosciente: niente uso silente di un
provider esterno.
Arabo, ebraico e persiano richiedono RTL nel rendering HTML dei canali
(layer 4 quando arriverà) e attenzione al markup
dir="rtl" nei template Jinja2. Layer 1-3 sono RTL-agnostici:
le stringhe sono solo testo, il canale e' responsabile del rendering.
| Comando | Effetto |
|---|---|
metnos-prompts list | Tabella prompt × lingue + size + last commit. |
metnos-prompts show <role> [--lang=it] | Render finale del prompt con vars stub. |
metnos-prompts validate | Lint sintassi MiniJinja + invariant check. |
metnos-prompts translate <role> [--to=en] [--quality=wise|frontier] | One-shot: traduce IT → --to; salva candidato in _pending/. |
metnos-prompts translate-all [--to=en] [--quality=wise|frontier] | Batch: traduce ogni .j2 mancante. |
metnos-prompts sync-status | Tabella mtime/lag/candidate per ruolo. |
metnos-prompts review <role> [--lang=en] | Mostra diff + validation di un candidato. Non promuove. |
metnos-prompts mark-synced <role> [--lang=en] | Promuove _pending a runtime se validation OK. |
metnos-prompts validate-cross-lang | Verifica placeholder + sintassi + len ratio cross-lang. |
metnos-prompts add-language <code> [--source-lang=it] | Bootstrap nuova lingua (layer 1+3, layer 2 al cycle successivo). |
Il manifest centrale di installazione (install/manifest.toml)
non elenca le lingue: sono dati persistiti, non capability. Aggiungere FR
non modifica i systemd unit né il manifest, solo
METNOS_LANG=fr al momento di attivarla.
indices_image: stesso schema di oggetto-strumento + qualifier dominio si applica a indices_text per ricerca semantica multilingua (futuro).get_inputs: i dialoghi richiedono prompt LLM in lingua del processo per il final_message_hint.get_inputs + on_complete: stesso prompt loader, stesse lingue.runtime/prompt_loader.py, runtime/i18n.py, runtime/i18n_translator.py, runtime/admin/prompts_cli.py, runtime/admin/i18n_cli.py.