Better I18NBetter I18N

sync

Compare local translation keys with Better i18n cloud

Compare your codebase's t() calls with translations stored in Better i18n (remote) and audit your project for consistency.

When to use sync

The sync command is a powerful auditing tool. Common scenarios include:

  • Before opening a PR: Ensure all new translation keys are present in Better i18n.
  • After adding new pages: Automatically identify and log missing keys.
  • After refactors: Detect "dead" keys that are no longer referenced in code.
  • Before shipping a new locale: Verify that the source keys are stable.
  • Debugging: Use --verbose to trace why certain keys are not being picked up.
  • CI Gating: Use --format json to fail builds if missing keys are detected.

Workflow

  1. Run sync: See the tree of missing or unused keys.
  2. Dashboard/AI: Add the missing keys via the Better i18n dashboard or AI suggestions.
  3. Verify: Run sync --summary to confirm missingCount is 0.
  4. Enforce: Integrate into CI to prevent missing keys from reaching production.

Usage

better-i18n sync                # Grouped tree output (default)
better-i18n sync --summary      # High-level metrics only
better-i18n sync --verbose      # Deep audit & verification
better-i18n sync --format json  # JSON output for automation
better-i18n sync -d ./src       # Scan specific directory

Options

OptionDescription
--format <type>eslint (default) for humans, json for machines.
--verboseDetailed audit log: invariant checks (PASS/FAIL), scoping summary, dynamic key skips, and specific key probes.
--summaryShow only top-level percentage coverage and counts without the detailed tree.
-d, --dir <path>Specify the directory to scan (default: current directory).

Output Example

The default output provides a 2-level compact tree of mismatches.

📊 Translation Keys Comparison
Source locale: en

Coverage:
  Local → Remote: 59%
  Remote Used: 63%

⊕ Missing in Remote (473 keys)
  Keys used in code but not uploaded to Better i18n

pages (300)
  affordableEnglishLearning (meta.title, meta.description, meta.keywords, hero.badge, ...+12)
  bestApps (hero.badge, title_prefix, title_accent, subtitle)

hero (5)
  hero (ariaLabel, imageAlt, ...)

⊖ Unused in Code (386 keys)
  Keys in Better i18n but not detected in code usage

features (25)
  practiceSpeaking (title, subtitle, icon)

Scanned 246 files in 0.85s
✓ Comparison complete

How namespace binding works

The CLI uses Lexical Scope Tracking to resolve namespaces automatically for both client and server components.

Client Components (React Hooks)

// All t() calls inside this scope become 'hero.key'
const t = useTranslations('hero');

return <h1>{t('title')}</h1>; // Detected as: hero.title

Server Components (Async Functions)

// ✅ Direct string namespace
const t = await getTranslations('welcome');
return <h1>{t('title')}</h1>; // Detected as: welcome.title

// ✅ Object with namespace property
const t = await getTranslations({
  locale: params.locale,
  namespace: 'maintenance'
});
return <p>{t('message')}</p>; // Detected as: maintenance.message

// ⚠️  Root scoped (no namespace)
const t = await getTranslations();
return <p>{t('global.title')}</p>; // Detected as: global.title (root scope)

Supported Patterns

  • useTranslations('namespace') - Client component with namespace
  • getTranslations('namespace') - Server component with namespace
  • getTranslations({ locale, namespace: 'namespace' }) - Server with locale
  • useTranslations() / getTranslations() - Root scoped (no namespace prefix)
  • Standard Methods: All patterns support t(), t.raw(), t.rich(), and t.has().

Dynamic Namespaces

If a namespace is a variable or template literal (e.g., useTranslations(`pages.${slug}`)), it is reported as unknown-scoped in --verbose mode and skipped from metrics to prevent false positives.


Flattening rules

To ensure CLI metrics match the Better i18n UI:

  1. Primitives are leaves: Strings, numbers, and booleans are counted as keys.
  2. Arrays are leaves: meta.keywords: ["i18n", "cli"] is counted as one key, not two. This matches the UI's "Base Key" counting.
  3. Objects are containers: Keys like pages.home.hero representing a grouping of sub-keys are ignored in the final key count.

JSON Output

Use --format json for CI scripts and automation.

{
  "project": {
    "workspace": "my-org",
    "slug": "my-app",
    "sourceLocale": "en"
  },
  "localKeys": {
    "total": 1223
  },
  "comparison": {
    "missingInRemote": { "pages.bestApps": ["pages.bestApps.meta.title"] },
    "unusedInCode": { "old": ["old.unused_key"] },
    "missingCount": 473,
    "unusedCount": 386
  },
  "coverage": {
    "local": 59,
    "remote": 63
  },
  "files": 246,
  "duration": 850
}

CI Integration

Fail build on missing keys

# Using jq to check missingCount
better-i18n sync --format json | jq -e '.comparison.missingCount == 0' > /dev/null || exit 1

List missing keys in CI logs

better-i18n sync --format json | jq '.comparison.missingInRemote'

On this page