doctor
Analyze your project's i18n health — missing translations, hardcoded strings, orphan keys, and CDN sync in one command.
Run a full i18n health check on your project in one command. doctor combines five analysis layers — code scanning, coverage, quality, performance, and CDN sync — and produces a single health score with actionable diagnostics.
When to use doctor
- Before opening a PR: Catch missing translations, hardcoded strings, and placeholder mismatches before review.
- In CI pipelines: Block merges when the health score drops below the pass threshold.
- Orphan key audits: Identify translation keys that are no longer used in code.
- Deployment checks: Verify that local keys are in sync with the remote CDN before shipping.
- First health baseline: Run once on an existing project to understand its i18n debt.
Usage
better-i18n doctor # Full analysis of current directory
better-i18n doctor --dir ./src # Scan a specific directory
better-i18n doctor --format json # JSON output for machine consumption
better-i18n doctor --ci # Exit code 1 if score below threshold
better-i18n doctor --report # Upload report to Better i18n dashboard
better-i18n doctor --report --api-key $KEY # Upload with explicit API key
better-i18n doctor --skip-sync # Skip CDN comparison
better-i18n doctor --skip-code # Skip hardcoded string detection
better-i18n doctor --skip-health # Skip translation file analysis
better-i18n doctor --verbose # Detailed per-file diagnostic outputOptions
| Option | Description |
|---|---|
-d, --dir <path> | Directory to scan (default: current directory) |
-f, --format <type> | Output format: eslint (human-readable, default) or json (machine) |
--ci | Exit with code 1 if health score is below the pass threshold (70) |
--report | Upload the report to Better i18n dashboard for tracking over time |
--api-key <key> | API key for report upload. Falls back to GitHub Actions OIDC if omitted. |
--skip-code | Skip AST-based hardcoded string detection |
--skip-health | Skip translation file health checks (coverage, quality, orphan keys) |
--skip-sync | Skip remote CDN comparison |
--verbose | Show detailed per-file diagnostics and verbose scan stats |
What it checks
doctor runs five categories of analysis in a single pass.
Code — hardcoded string detection
Scans your source files with an AST parser to detect user-facing strings that are not wrapped in a translation function.
| Rule | What it catches | Example |
|---|---|---|
jsx-text | Hardcoded text nodes in JSX | <h1>Welcome back</h1> |
jsx-attribute | Hardcoded attribute values | <img alt="Company logo" /> |
ternary-locale | Inline locale-based string logic | locale === 'en' ? 'Hi' : 'Hola' |
toast-message | Hardcoded toast/notification text | toast.error("Something went wrong") |
string-variable | String variables assigned to UI text | const label = "Submit" |
Coverage — missing translations
Compares keys present in your source locale against each target locale. Any key that exists in the source but is missing from a target locale is reported.
Quality — placeholder mismatch
Verifies that interpolation placeholders are consistent across locales. Supports all common formats:
| Format | Example |
|---|---|
Named {} | {name}, {count} |
Double-brace {{}} | {{username}} |
printf %s | %s, %d |
Template ${} | ${value} |
Positional {0} | {0}, {1} |
A source key with Hello, {name}! and a translation with Hola! (placeholder removed) is a quality error.
Performance — orphan keys
Detects keys that exist in your translation files (or remote CDN) but are never referenced in code. Orphan keys increase payload size and create maintenance debt.
Sync — CDN comparison
Compares keys extracted from your code against the published keys in the Better i18n CDN. Requires workspaceId and projectSlug in i18n.config.ts.
| Issue | Meaning |
|---|---|
missing-in-remote | Key used in code but not yet published to CDN |
unused-remote-key | Key published to CDN but not found in code |
Health Score
doctor computes a score from 0 to 100 based on the diagnostics found.
Overall score formula:
score = 100 - (errors × 3.0) - Σ min(rule_warnings × 0.15, 20)Each rule's warning contribution is capped at 20 points. This prevents a single rule with thousands of warnings (e.g. missing-in-remote after a large migration) from zeroing your entire score.
Grade thresholds:
| Grade | Score range | CI result |
|---|---|---|
| A+ | ≥ 90 | Pass |
| A | ≥ 80 | Pass |
| B | ≥ 70 | Pass |
| C | ≥ 50 | Fail |
| F | < 50 | Fail |
The default pass threshold is 70. Scores below this cause --ci to exit with code 1.
Output Example
╭──────────────────────────────────────────────╮
│ │
│ 🌐 better-i18n · i18n Doctor Report │
│ hello · hola · 你好 · こんにちは · 안녕 │
│ │
├──────────────────────────────────────────────┤
│ ████████████████░░░░ 82 / 100 A │
│ PASSED (threshold: 70) │
╰──────────────────────────────────────────────╯
Category Scores:
Coverage 95 (3 issues)
Quality 88 (2 issues)
Code 72 (8 issues)
Structure 100 (clean)
Performance 91 (1 issues)
8 warnings, 6 info
214 files scanned, 1847 keys checked, 5 locales
Completed in 1.24s
⚠ Hardcoded JSX text detected (8)
Wrap with t() to enable translation
src/components/Navbar.tsx: 12, 34
src/pages/settings.tsx: 88
src/components/Footer.tsx: 6, 19
... and 5 more files
⚠ Key "auth.signup.cta" found in code but not in remote translations (3)
Run `better-i18n sync` to upload missing keys
default/auth.signup.cta
default/auth.login.subtitle
default/onboarding.step3.titleJSON Output
Use --format json to get machine-readable output for custom tooling or dashboards.
better-i18n doctor --format json
better-i18n doctor --format json | jq '.score.total'
better-i18n doctor --format json | jq '[.diagnostics[] | select(.severity == "error")]'The JSON report follows this structure:
{
"runAt": "2025-03-12T10:00:00.000Z",
"durationMs": 1240,
"git": {
"commit": "a1b2c3d",
"ref": "main",
"repository": "org/repo"
},
"score": {
"total": 82,
"passed": true,
"passThreshold": 70,
"categories": {
"Coverage": 95,
"Quality": 88,
"Code": 72,
"Structure": 100,
"Performance": 91
}
},
"summary": {
"total": 14,
"errors": 0,
"warnings": 8,
"infos": 6,
"byCategory": { "Code": 8, "Coverage": 3, "Quality": 2, "Performance": 1 },
"filesScanned": 214,
"keysChecked": 1847,
"localesChecked": 5
},
"diagnostics": [
{
"filePath": "src/components/Navbar.tsx",
"line": 12,
"column": 8,
"rule": "jsx-text",
"category": "Code",
"severity": "warning",
"message": "Hardcoded JSX text: \"Sign in\"",
"help": "Wrap with t() to enable translation"
}
]
}CI Integration
GitHub Actions — automatic auth
When running in GitHub Actions with OIDC enabled, --report authenticates automatically without requiring an explicit API key:
name: i18n Health Check
on: [push, pull_request]
permissions:
id-token: write # Required for OIDC
jobs:
i18n-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: npx @better-i18n/cli doctor --ci --reportManual API key
- run: npx @better-i18n/cli doctor --ci --report --api-key ${{ secrets.BETTER_I18N_API_KEY }}Exit code behavior
When --ci is set:
- Score ≥ threshold: exits with code 0 (pass)
- Score < threshold: exits with code 1 (fail)
--reportsucceeds: exits with code 0 even if score fails — the report is uploaded to the dashboard for tracking
This last rule lets you start tracking health without immediately blocking CI. Once your baseline is established, remove --report (or add both) to enforce the threshold.
Configuring Rules
Disable or downgrade rules in i18n.config.ts under the lint.rules key:
export default defineConfig({
// ...
lint: {
rules: {
// Turn off rules that don't apply to your project
"orphan-keys": "off",
"string-variable": "off",
// Downgrade from error to warning
"missing-translations": "warning",
"placeholder-mismatch": "warning",
},
},
});| Value | Behavior |
|---|---|
"error" | Counts toward error penalty (−3.0 per occurrence) |
"warning" | Counts toward warning penalty (capped at −20 per rule) |
"off" | Rule is skipped entirely |
Skipping Analysis Steps
Use skip flags to run only the analysis layers you care about:
# Only check translation file health (no code scan, no CDN)
better-i18n doctor --skip-code --skip-sync
# Only check CDN sync status (fast — no AST parsing)
better-i18n doctor --skip-code --skip-health
# Only scan for hardcoded strings
better-i18n doctor --skip-health --skip-sync