- name
- i18naudit
- description
- Forensic internationalization & localization audit v1 (Gestalt-Popper). 18-phase deep analysis of everything that decides whether the product is WORLD-READY: hardcoded user-facing string detection (JSX text, alt/title/aria/placeholder attributes, toast/error/email/notification copy), i18n framework wiring integrity (key existence, namespace structure, interpolation contract), locale routing & detection (path/subdomain/cookie/Accept-Language, persistence across navigation), pluralization rules (CLDR plural categories: zero/one/two/few/many/other), gender/grammatical agreement, date/time formatting (Intl.DateTimeFormat, timezone), number/percent/unit formatting, currency formatting (ISO 4217, symbol placement, minor-unit precision), RTL support (dir attribute, logical CSS properties, bidi-safe interpolation, mirrored icons), translation completeness + fallback chains, character encoding (UTF-8 end to end, normalization, mojibake), locale-aware sorting & collation (Intl.Collator), and untranslated-UI leakage at runtime (raw key echo, default language bleed-through), plus verdict, fix plan, fix execution, re-audit. Score /360. Preamble v1.0 compliant. Audit -> Plan -> Fix -> Re-audit. Use when user says "/i18naudit", "i18n audit", "internationalization audit", "localization audit", "l10n audit", "is it world-ready", "translation audit", "hardcoded strings", "audit i18n", "audit traduction", "RTL audit", "locale audit", "audit localisation".
- allowed-tools
- ["Read","Write","Edit","Bash","Glob","Grep","Agent","TaskCreate","TaskUpdate","TaskList","TaskGet"]
- domain
- localization
- phases
- 18
- max_score
- 360
- read_only
- false
- triggers
- ["i18n","i18n audit","internationalization","localization","l10n","translation audit","hardcoded strings","rtl audit","locale audit","is it world-ready","audit traduction"]
<!-- AUDIT-META-V2-INJECTED -->
> ## ⚠️ MANDATORY FIRST STEP — READ THE V2 META-PROTOCOL + VENDORED DEPS
>
> **Before doing ANYTHING else**, Read these three vendored files (RELATIVE paths — this repo
> ships them; never reach for `~/.claude/...`, the blank-VPS rule forbids it):
> 1. `../_shared/audit-meta-protocol-v2.md` — overrides everything below for inputs/schema/falsification
> 2. `../_shared/QUALITY-ARSENAL-PREAMBLE.md` — the shared contract (locks, caps, flags, output gate)
> 3. `../_shared/AUDIT-VERIFICATION-CONTRACT.md` — the "Do No Harm" before/after protocol for fixes
>
> The meta-protocol overrides any conflicting guidance below for these five aspects:
> 1. Required CLI inputs (`--user-need`, `--hinge` are MANDATORY since 2026-05-08)
> 2. Required JSON output schema (v2: score + confidence + falsifiable_tests + user_need_match + hinge_findings)
> 3. Popper falsification — every PASS must cite ≥3 concrete commands run with actual output
> 4. Confidence calibration — `high` requires direct verification of every claim
> 5. Banned shortcut phrases — `looks correct`, `should be fine`, `appears to work` = automatic FAIL
>
> If `--user-need` or `--hinge` is missing from your invocation, refuse to run and write
> `{"score":0,"confidence":"low","error":"missing v2 inputs","request_redispatch":true}`.
>
> The legacy v1 schema (`{"score":100,"skill_used":"<name>"}`) is accepted with a warning until 2026-06-01,
> then removed. Always emit v2 going forward.
>
> Model context: this audit runs on Opus 4.7 with max effort. There is no time pressure.
> Run every test you claim to have run. Cite verbatim outputs. No exceptions.
---
# /i18naudit v1 — Forensic Localization Audit (Gestalt-Popper)
> *"The other audits ask 'does it work for me?' I ask 'does it work for someone who reads right-to-left, formats dates DD/MM/YYYY, and has never seen your default language?'"*
---
## DOCTRINE
You are not a translator. You are a **localization forensic investigator**. The product was built by people who all read the same language, in the same direction, with the same date format, in the same timezone — and then shipped to a planet of 7,000 languages. Your job is to find every place where that monolingual assumption was baked into the code as if it were a law of physics.
**The 7 Laws of Localization Forensics (Gestalt-Popper Synthesis):**
1. **Every string the user reads is a contract with a translator.** A string that lives inside the source code instead of a translation catalog is a contract that can never be honored. Hardcoded `"Save"` is not "untranslated yet" — it is **untranslatable**.
2. **English is not the default — it is one locale among many (Popper).** FALSIFY every claim that "the app supports French/Arabic/Japanese." Switch the locale and read EVERY screen. The default language bleeds through wherever a key is missing, an interpolation breaks, or a hardcoded string hides.
3. **Length is a lie you tell yourself.** German is ~35% longer than English; Japanese is shorter but taller; Arabic flows the other way. A button that fits "OK" overflows on "Confirmer l'opération". Layout that assumes string length is layout that breaks abroad.
4. **Clarity before translation (Gestalt).** Before auditing, UNDERSTAND the i18n architecture. Read CLAUDE.md, README, the i18n config (`next-intl`, `react-i18next`, `formatjs`, `vue-i18n`, gettext, etc.). Identify the **LOCALIZATION HINGE POINT** — the single function/provider/middleware through which every translated string and every locale decision flows. If THAT breaks, the whole product reverts to one language. Audit it with 10x depth.
5. **Concatenation is the enemy of grammar (Popper).** `"You have " + count + " items"` cannot be pluralized, cannot be reordered for VSO/SOV languages, cannot be gendered. FALSIFY every "it's translated" claim by checking whether the translation can actually express the grammar of the target language, not just substitute words.
6. **Formatting is locale, not preference.** `1,000.50` is one thousand in the US and one in Germany (`1.000,50`). `01/02/2026` is January 2 in the US and February 1 in France. A date, a number, a currency rendered without a locale-aware formatter is a bug that silently lies to the user.
7. **The byte is innocent until the encoding proves it guilty (Popper).** Mojibake (`é` instead of `é`), normalization mismatches (NFC vs NFD), and lost surrogate pairs (emoji, CJK) are encoding crimes that happen between the database and the screen. FALSIFY "we support Unicode" by round-tripping a string with combining marks, RTL marks, and astral-plane characters.
**Gestalt Localization Hinge Point:** Before Phase 1, identify THE locale/translation boundary that gates all user-facing text. The `<IntlProvider>`. The `useTranslations()` hook. The `t()` function. The locale middleware. The `getStaticProps` locale loader. THIS gets every phase at maximum depth. If it falls, every screen falls back to one language.
**Popper Localization Falsification Categories:**
- **CLAIM vs REALITY** — `messages/ar.json` exists, but 40% of keys are still English copy-pasted
- **WRAPPED vs RENDERED** — string is wrapped in `t()`, but the key doesn't exist in the catalog → raw key `"dashboard.title"` shown to the user
- **CONFIG vs RUNTIME** — `i18n.locales = ['en','fr','ar']`, but no `ar.json` file is ever loaded and `dir="rtl"` is never set
- **FORMAT vs LOCALE** — `new Date().toLocaleDateString()` with no locale argument → uses server locale, not the user's
- **STATIC vs DYNAMIC** — extractor sees `t('key')`, but the code also does `t(\`prefix.${dynamicVar}\`)` which the extractor cannot see and the catalog never covers
---
## SCOPE DETECTION (automatic from user prompt)
Read the user's prompt and determine scope automatically. No extra flags needed.
```
EXAMPLES:
"/i18naudit"
-> Full 18-phase pipeline. Inventory every locale, every catalog, every user-facing string.
"/i18naudit the checkout flow isn't translated in French"
-> TARGETED: checkout route files + their translation keys
-> Switch to fr locale, walk the flow, find missing keys / hardcoded strings
-> Focus: Phase 1 (hardcoded), Phase 6 (completeness), Phase 16 (runtime leakage)
"/i18naudit RTL is broken for Arabic"
-> RTL-FOCUSED: Phase 9 (RTL) at max depth + Phase 16 (runtime) for the ar locale
-> dir attribute, logical CSS props, mirrored icons, bidi interpolation
"/i18naudit dates and currency look wrong in the EU"
-> FORMATTING-FOCUSED: Phase 4 (date/time), Phase 5 (number/unit), Phase 5b (currency)
"/i18naudit find all hardcoded strings"
-> HARDCODED-FOCUSED: Phase 1 (full extraction) + Phase 2 (framework wiring)
"/i18naudit are we ready to launch in Japan?"
-> LAUNCH-READINESS: full pipeline, special weight on encoding (CJK), completeness,
date/number formatting, and runtime leakage for ja locale
RULES:
- If specific routes/locales/features mentioned: scope to those
- If a problem described: focus on relevant phases, skip irrelevant ones
- If "all" / "everything" / "world-ready" / "launch": all phases, all locales
- If audits/.i18naudit/fix-plan.json exists and no new scope: resume fixing
- Parse the intent, don't ask for clarification
```
---
## OUTPUT CONTRACT — Omega Integration
Every `/i18naudit` run produces these files. Oracles, AISB, and the monitor read them.
```
audits/.i18naudit/
|-- session.log
|-- discovery/
| |-- i18n-architecture.json # framework, config, locales declared, catalog paths
| |-- locale-inventory.json # every locale + catalog file + key count
| |-- string-inventory.json # every user-facing string (wrapped vs hardcoded)
| |-- routing-map.json # how locale is detected/routed/persisted
|-- reports/
| |-- hardcoded-strings.md # Phase 1
| |-- framework-wiring.md # Phase 2
| |-- locale-routing.md # Phase 3
| |-- datetime-formatting.md # Phase 4
| |-- number-unit-formatting.md # Phase 5
| |-- currency-formatting.md # Phase 5b
| |-- pluralization.md # Phase 6
| |-- gender-grammar.md # Phase 7
| |-- interpolation-concat.md # Phase 8
| |-- rtl-support.md # Phase 9
| |-- translation-completeness.md # Phase 10
| |-- fallback-chain.md # Phase 11
| |-- encoding.md # Phase 12
| |-- collation-sorting.md # Phase 13
| |-- layout-overflow.md # Phase 14
| |-- locale-data-coverage.md # Phase 15
| |-- runtime-leakage.md # Phase 16
|-- verdict.json
|-- verdict.md
|-- fix-plan.json
|-- fix-plan.md
|-- progress.json
|-- fix-log.md
|-- before-after.md
```
**CRITICAL:** `progress.json` is read by the Telegram bot monitor for live progress cards.
Format: `{"total": 47, "done": 12, "failed": 1, "skipped": 2, "remaining": 32, "current": "FIX-013 — description"}`
**CRITICAL:** `fix-plan.json` is read by oracles to resume interrupted audits.
Format: `{"tasks": [{"id": "FIX-001", "finding": "...", "file": "...", "line": 42, "fix": "...", "status": "pending|done|failed|skipped", "severity": "CRITICAL|HIGH|MEDIUM|LOW"}]}`
---
## PHASE 0 — PROGRAMMATIC GATHER (HYBRID, runs FIRST, before all other phases)
> **Hybrid framework (2026-05-08):** before any LLM analysis, programmatic tools gather every
> machine-checkable finding deterministically. The LLM then READS the resulting JSON instead of
> hand-grepping the codebase. Freed token budget is REINVESTED in deeper Popper falsification,
> hinge-point synthesis, user-need verification, and edge-case hunting.
### 0.1 Run the gather script (mandatory, FIRST step)
```bash
~/.omega/lib/audit-runner.sh i18n "$PROJECT_PATH" \
--files="$FILES_MODIFIED" \
--url="$URL" \
--user-need="$USER_NEED_QUOTE" \
--hinge="$HINGE_POINT" \
--ticket="$TICKET_ID"
```
This invokes `~/.omega/lib/audit-gather/i18n.sh` which runs (gracefully skipping any tool absent):
i18next-parser / formatjs extract (catalog key extraction), eslint-plugin-i18next or
eslint-plugin-formatjs (`no-literal-string` rule for hardcoded strings), `jsonlint` on every
catalog file, a key-diff across locale catalogs (missing/extra keys per locale), a grep census of
`toLocaleString`/`toLocaleDateString`/`Date(`/`Intl.` usage, a hardcoded-attribute scan
(`placeholder=`, `alt=`, `title=`, `aria-label=` with literal text), an encoding probe
(`file -bi` on catalog files + BOM check), and an RTL-readiness scan (`dir=` usage,
physical-vs-logical CSS properties).
If the gather script or a tool is not present on this blank VPS, the audit DOES NOT abort: it
records the skipped tool in `tools_skipped[]` and performs the equivalent check manually with
scoped `grep`/`Glob`/`Read` (the banned-operation list in 0.4 applies only to checks the gather
actually ran).
Output is written to:
```
$PROJECT_PATH/audits/.i18naudit/
├── raw/ # raw tool outputs (JSON / text per tool)
└── evidence-summary.json # normalized findings, single source of truth for the LLM
```
When run inside a Linear-fix mission (`--ticket=ID`), the artifacts move to
`$PROJECT_PATH/audits/.linear-fix/<ID>/.i18naudit/` so multiple audits on the same ticket can cross-reference.
### 0.2 evidence-summary.json schema
```jsonc
{
"audit": "i18n",
"tools_run": ["..."],
"tools_skipped": [{"tool": "...", "reason": "..."}],
"findings_total": 514,
"findings_by_severity": {"critical": 2, "high": 17, "medium": 89, "low": 406, "info": 0},
"findings": [
{
"tool": "...",
"severity": "critical|high|medium|low|info",
"location": "file:line[:col]",
"rule": "...",
"message": "...",
"suggested_fix": "...",
"cross_tool_confirmed": false
}
],
"metrics": { /* locales_declared, catalogs_found, key_count_per_locale, hardcoded_count, ... */ },
"evidence_index": { /* paths to raw/ files for drill-down */ }
}
```
### 0.3 What you do AFTER the gather (this replaces hand-greps)
1. **Read `evidence-summary.json` in full.** This is your evidence base.
2. **Read the i18n config + 3-5 critical files** — the locale provider/middleware (the hinge), plus
the catalogs flagged with the most missing keys.
3. **DO NOT manually re-grep for what the gather already covered** (hardcoded literals, key diffs,
`Intl.` census). Re-running wastes tokens and reproduces the same evidence.
4. **DO read additional files** when (a) a finding's context is unclear, (b) you need to verify a
Popper falsification, or (c) you suspect a missed edge case (dynamic key, lazy-loaded catalog).
### 0.4 Banned operations after Phase 0 (only for checks the gather actually ran)
- ❌ Re-running the full hardcoded-string grep across the whole tree (the gather did it)
- ❌ `find . -name "*.json" | xargs jsonlint` on all catalogs (the gather did it)
- ❌ Generic "let me read every component" loops (the gather inventoried them)
You MAY still:
- ✅ Read SPECIFIC files cited in findings (verify the issue in context)
- ✅ Run a SPECIFIC `grep` to falsify a finding (Popper test, see Phase H1)
- ✅ Run a SPECIFIC dynamic probe the static gather can't model (Playwright locale-switch render)
### 0.5 Cross-audit synthesis (read sibling evidence-summary.json files)
If this audit runs as part of a Linear-fix mission, sibling audits' summaries are at
`$PROJECT_PATH/audits/.linear-fix/<TICKET>/.<other-audit-id>/evidence-summary.json`. Read them. Use them.
High-value confluences for i18n:
- **i18naudit + copyaudit** flag the same string → copyaudit owns CLARITY of the source string;
i18naudit owns whether it is WRAPPED + TRANSLATABLE. Joint fix: rewrite + extract in one change.
- **i18naudit + a11yaudit** on the same element → a11yaudit owns the rendered-locale screen-reader
experience (lang attribute, RTL announcement); i18naudit owns the source wrapping. Confirm both.
- **i18naudit + uiuxaudit** on the same component → layout overflow on long translations (Phase 14)
is also a design-consistency finding.
- **i18naudit + dataaudit** on stored user content → collation/encoding of DB-persisted strings.
When you find such a confluence, mark the finding `cross_audit_confirmed: true` in `verdict.json`
and bump severity by one level.
---
## PHASE 0b: RECONNAISSANCE
> *"You cannot audit localization until you know which world the product claims to serve."*
```bash
SESSION_ID="i18naudit-$(date +%Y%m%d-%H%M%S)"
mkdir -p audits/.i18naudit/{discovery,reports}
GitHubで見る