La sezione i18n qui sotto è quella da incollare in AGENTS.md: dieci regole brevi che dicono a un coding agent di far passare ogni stringa visibile all'utente dalla funzione di traduzione, di assegnarle una chiave organizzata per feature, di non concatenare mai frammenti, di non simulare i plurali con un ternario e di non toccare mai un file locale generato. globalize.now è un'infrastruttura di localizzazione basata su IA, quindi leggiamo molto codice i18n scritto da agent, e quasi tutto si riduce agli stessi pochi errori. Ogni regola corrisponde a uno di essi.

La seconda metà di questo post riguarda il punto in cui il file smette di funzionare. Due delle superfici di codice più usate dagli sviluppatori non lo leggono mai, e nessuna riformulazione risolve il problema.

Cosa deve dire la sezione i18n di AGENTS.md?

Questo blocco. Adatta i tre percorsi dei file e il comando di lint al tuo progetto e lascia il resto così com'è.

## Internationalization

- Every user-visible string goes through the translation function. That
  includes JSX text, input hint text, `title`, `aria-label`, `alt`, toast and
  error messages, empty states and the document `<title>`.
- The source locale is `en`. The catalog is `locales/en.json`. Add the key in
  the same change that uses it.
- Keys are namespaced by feature: `checkout.summary.total`, not `total3`.
  Search the catalog for an existing key before creating one.
- Never concatenate translated fragments. Use interpolation:
  `t('cart.items', { count })`, not `t('cart.you_have') + count`.
- Plurals use the library's plural forms (`_one` / `_other` keys, or ICU
  `{count, plural, ...}`). Never `count === 1 ? 'item' : 'items'`.
- Dates, numbers and currency go through `Intl.DateTimeFormat` and
  `Intl.NumberFormat` with the active locale. Never a bare
  `toLocaleString()`, never a currency symbol glued to a number.
- Do not hardcode the locale list, language names or text direction in
  components. Read them from `i18n/config.ts`.
- Do not translate brand names, code identifiers, URLs or environment values.
- Do not edit locale files other than `en`. They are generated. If a
  translation looks wrong, fix the source key or the glossary, not the file.
- Before finishing, run `npm run lint`. The `i18next/no-literal-string`
  rule must pass.

Dieci regole è una scelta deliberata. La documentazione delle regole di Cursor dice di concentrarsi sui pattern usati di frequente e di usare un linter invece di incollare una style guide, e le indicazioni di Claude Code vanno nella stessa direzione: istruzioni abbastanza concrete da essere verificabili, e un file corto perché l'aderenza cala man mano che cresce. Una sezione i18n di quaranta punti elenco viene saltata a pie' pari.

Perché proprio queste dieci regole e non un semplice «usa i18n»?

Perché «usa i18n» è esattamente ciò che l'agent crede già di fare quando scrive <Button>Save changes</Button>. Le istruzioni vaghe vengono soddisfatte dall'idea di conformità dell'agent stesso. Ogni regola qui sotto indica un output preciso e lo vieta.

Literal negli attributi. Gli agent imparano che i children JSX vanno tradotti e poi scrivono aria-label="Close" e alt="Company logo" come stringhe semplici, perché gli attributi sembrano configurazione e non testo. Elencare gli attributi per nome chiude il buco; un generico «traduci tutto» no.

Concatenazione. t('greeting') + ' ' + name + '!' in inglese funziona, ma non si può tradurre in nessuna lingua che metta il nome per primo o lo flette. L'interpolazione dà a chi traduce una sola stringa con una sola variabile da spostare.

Plurali con ternario. count === 1 ? 'item' : 'items' è ciò che vediamo scrivere più spesso a un agent dopo che l'i18n è stata configurata, ed è sbagliato per ogni lingua con più di due forme plurali. La regola nomina il pattern, così l'agent può riconoscerlo. Se il tuo catalogo usa ICU, cos'è ICU MessageFormat e dove si rompe nelle app generate dall'IA copre la sintassi.

File locale generati. Questa regola esiste perché gli agent sono servizievoli. Se gli chiedi di correggere un refuso in tedesco, aprirà locales/de.json e lo modificherà, e va bene finché il prossimo job di traduzione non rigenera il file dal sorgente. Dirgli che i file non sorgente sono generati, e dove va fatta la correzione vera, evita un'intera classe di regressioni silenziose.

Modificare en nella stessa modifica. Senza questa regola l'agent aggiunge t('checkout.summary.total') e passa oltre, e la chiave viene mostrata con il proprio nome finché qualcuno non se ne accorge. La voce del catalogo e il suo primo utilizzo vanno nello stesso diff.

Dove legge il file ogni strumento?

Lo stesso contenuto va in posti diversi a seconda dell'agent, e la posizione decide se la regola è sempre nel contesto o viene caricata solo quando serve.

AGENTS.md nella root del repository è la posizione condivisa. Il formato è Markdown semplice senza campi obbligatori, è gestito dalla Agentic AI Foundation sotto la Linux Foundation ed è letto da Cursor, Codex, dal coding agent di Copilot e da una lunga lista di altri. La specifica supporta i file annidati, e vince quello più vicino al file modificato.

Cursor legge direttamente AGENTS.md, anche quelli annidati, e ha anche un proprio formato con scope. Una regola limitata ai file della UI resta fuori dal contesto finché non si apre un file corrispondente:

---
description: i18n rules for user-visible strings
globs: src/**/*.tsx, src/**/*.jsx
alwaysApply: false
---
(paste the Internationalization section here)

Il file deve terminare in .mdc e stare in .cursor/rules/; un semplice .md in quella cartella viene ignorato perché non ha frontmatter.

GitHub Copilot in VS Code offre tre opzioni. .github/copilot-instructions.md si applica a ogni richiesta in chat. Un file .github/instructions/i18n.instructions.md con applyTo: "**/*.tsx,**/*.jsx" nel frontmatter si applica solo quando quei file vengono creati o modificati. Infine, il file AGENTS.md nella root del repository viene letto dal Local agent quando l'impostazione chat.useAgentsMdFile è attiva; i file AGENTS.md annidati nelle sottocartelle dipendono da un'altra impostazione sperimentale, chat.useNestedAgentsMdFiles, disattivata di default. Il comportamento specifico di Copilot è trattato in come aggiungere l'i18n a un'app creata con GitHub Copilot.

Claude Code legge CLAUDE.md e, dalla v2.1.277, legge da solo un AGENTS.md nella root quando il progetto non ha nessun CLAUDE.md sopra la directory di lavoro. Se tieni entrambi, il pattern documentato è un import @AGENTS.md in cima a CLAUDE.md, così il file viene caricato una volta sola. Per lo scope, .claude/rules/i18n.md con un elenco paths: nel frontmatter viene caricato solo quando Claude legge un file corrispondente. Il workflow completo di Claude Code è in come localizzare un'app generata da Claude Code.

L'assetto pratico per un repo toccato da più agent: le dieci regole in un file con scope per l'agent che il tuo team usa di più, e un rimando di due righe in AGENTS.md («Stringhe della UI: vedi le regole i18n in .cursor/rules/i18n.mdc e seguile»), così anche un agent senza supporto allo scope riceve l'istruzione.

Quali superfici di codice non leggono mai AGENTS.md?

I completamenti inline. Lo documentano entrambi i vendor, ed è il motivo per cui il file di regole non può essere l'intera risposta.

Nelle FAQ sulle regole, Cursor risponde con un no secco alla domanda «Do rules impact Cursor Tab or other AI features?». Le regole alimentano Agent; Tab, l'autocompletamento che chiude la riga che stai scrivendo, non le vede. La pagina delle istruzioni personalizzate di VS Code riporta la stessa nota per Copilot: le istruzioni non vengono considerate per i suggerimenti inline mentre scrivi nell'editor.

Quindi la superficie in cui uno sviluppatore scrive <p>No results found</p> e accetta il ghost text è esattamente quella che le regole non raggiungono mai. Chat e modalità agent assegneranno la chiave alla stringa; il completamento che ha chiuso la riga mentre pensavi ad altro no. In una settimana di lavoro normale il codebase accumula literal dall'unico percorso che il file di istruzioni non copre, e lo sviluppatore, che ha scritto con cura il suo file di regole, pensa che l'agent lo stia ignorando.

Claude Code non ha una superficie di completamento inline, ma la sua documentazione sostiene lo stesso concetto dall'altro lato: il contenuto di CLAUDE.md è contesto, non configurazione imposta, e per bloccare un'azione indipendentemente da ciò che decide il modello si usa un hook. È il modello mentale giusto per tutti gli strumenti qui. I file di istruzioni spostano le probabilità. Non impongono nulla.

Cosa fa rispettare davvero la regola?

Una regola di lint in CI, ed è una modifica di una riga se il file di regole dice già all'agent di eseguirla. eslint-plugin-i18next include no-literal-string; attivala per le directory dei componenti e un literal in JSX fa fallire la build invece di ricevere un promemoria gentile. Setup, opzioni della regola e perché istruzione e enforcement sono due livelli distinti sono spiegati in perché Cursor continua ad aggiungere stringhe hardcoded dopo il setup i18n, quindi in questo post non li ripetiamo.

Il livello di lint ti dà due cose che il file di regole non può darti. Intercetta l'output dei completamenti inline, perché lavora sul file e non sulla conversazione. E trasforma il loop dell'agent stesso nella correzione: l'ultima regola del blocco qui sopra dice all'agent di eseguire il lint prima di chiudere, e un agent che vede fallire no-literal-string assegnerà lui stesso la chiave alla stringa nella stessa sessione.

Per Claude Code in particolare, un hook PreToolUse o post-edit che esegue il linter sul file appena scritto è il meccanismo di enforcement a cui rimanda la sua documentazione. Gli hook girano come comandi shell in punti fissi e valgono a prescindere dal fatto che il modello abbia deciso di seguire la regola.

Che ne è delle stringhe che passano comunque?

Vanno estratte, associate a una chiave nei namespace esistenti e tradotte, ed è questa la parte da automatizzare invece di rilanciare prompt. Una build rossa ti dice che esiste un literal. Qualcuno deve comunque trasformarlo in una chiave, aggiungerlo a en e portarlo in tutte le altre lingue.

È il livello in cui si colloca globalize.now. La conversione è una tantum e avviene nell'app: connetti il repository e il codebase viene convertito una volta sola, con il catalogo consegnato come pull request da revisionare. Poi i push job traducono le nuove unità del catalogo man mano che arrivano, così una chiave aggiunta nella PR di martedì ha le sue traduzioni mercoledì. La libreria di runtime, il formato del catalogo e il file di regole qui sopra restano tutti tuoi; la documentazione per sviluppatori spiega come gli agent in Cursor, Claude Code, Codex e Copilot assumono il ruolo post-setup, e l'integrazione con Cursor è la guida più rapida.

Due modalità di errore correlate hanno un post dedicato. Se il problema è l'agent che riscrive testi già associati a una chiave, trovi come impedire agli agent IA di riscrivere i testi della tua UI, e la soluzione è la stessa disciplina sul source locale della regola nove qui sopra. Se le lingue esistono ma continuano a divergere, perché i file di traduzione vanno fuori sincrono spiega il problema di routing che c'è dietro.

Da dove si parte?

Incolla il blocco, limitalo alle directory della UI e attiva no-literal-string in CI nel giro del pomeriggio. Poi osserva cosa intercetta la regola di lint in una settimana: è la misura di quanto il file di regole avrebbe mai potuto fare da solo. Se stai rilasciando un'app creata con l'IA e vuoi che estrazione e traduzione di tutto ciò che sfugge siano gestite per te anziché mantenute da te, la pagina per i vibe coder è la panoramica e i prezzi hanno una pagina a parte.

globalize.now trasforma il testo hardcoded dell'app in file di localizzazione pronti e li mantiene aggiornati mentre rilasci.

Prova globalize.now gratis