Add i18n checks to your CI pipeline so new hardcoded strings get caught before production. ESLint rules, AST-based scanning, and coverage gates explained.
You wouldn't ship code without running your linter. But most teams ship code with hardcoded, untranslated strings on every release — because they don't have an i18n linter in their CI pipeline.
i18n linting is the practice of automatically checking your codebase for strings that should be translated but aren't. It catches regressions before they reach production: a developer adds a new feature, hardcodes the button labels in English, and the i18n lint step flags them in the PR before the code merges.
Here's how to set it up, from basic ESLint rules to comprehensive AST-based detection.
The simplest i18n lint is an ESLint rule that flags hardcoded strings in JSX.
Flags strings not wrapped in i18next's t() function:
{
"plugins": ["i18next"],
"rules": {
"i18next/no-literal-string": ["warn", {
"markupOnly": true,
"ignoreAttribute": ["className", "style", "type", "key", "id", "data-testid"]
}]
}
}
A stricter rule that flags all JSX text content:
{
"rules": {
"react/jsx-no-literals": ["warn", {
"noStrings": true,
"allowedStrings": ["—", "·", "×"]
}]
}
}
ESLint rules do run on an AST, but these i18n rules mostly judge each literal by its node type and a list of ignored attributes — not by whether it ends up in the UI. This means:
className="flex", type="button", href="/about" all get flagged unless you manually exclude every non-translatable attribute.Alert.alert().jsx-no-literals often turn it off again because real issues get buried under warnings.Verdict: Good as a basic gate. Not reliable enough to be your primary i18n lint.
Polyglot's scanner uses tree-sitter to parse your source code into a full syntax tree, then walks it to find strings based on their structural context. This cuts down false positives — className values aren't flagged because the AST knows they're className attributes, not user-facing text.
Add the open Polyglot Action to your GitHub Actions workflow:
# .github/workflows/i18n-lint.yml
name: i18n Lint
on: [pull_request]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for new untranslated strings
uses: polyglot-i18n/[email protected]
with:
check-mode: differential
Differential findings. The Action compares your PR against its base and separates new strings from existing debt and resolved findings. It needs a committed polyglot.toml. The default no-new policy fails the job only when a PR adds a finding. Want to watch first? Set preset = "informational" under [ci] in polyglot.toml while you establish a baseline, then switch back and make it a required check to block merges. On Team and Scale, Polyglot Automation (early access) adds a native GitHub Check with shared policy and history.
Fewer false positives. Because detection is AST-based, the CI check filters out CSS classes, import paths, and URLs by structure. Findings are still candidates, so the occasional one needs a human call, but there's far less noise to wade through.
Beyond per-PR linting, polyglot coverage reports, per target language, how many catalog keys have translations. Run it on your default branch (or read the Action's average_coverage output) to measure progress:
# Current coverage
polyglot coverage
Output:
Translation Coverage (76 strings across 214 files)
────────────────────────────────────────────────────────────
Language Translated Coverage
─────────── ──────────── ─────────
fr 1247/1323 94.3% [█████████░]
de 1241/1323 93.8% [█████████░]
ja 1205/1323 91.1% [█████████░]
Overall: 93.0% average coverage
Tracking coverage as a metric — like test coverage — makes i18n visible to the team. When coverage is a number that shows up in PRs and dashboards, it gets attention. Add --by-file for a per-file breakdown, or --format badge for a README badge.
The best setup uses both:
i18next/no-literal-string as a development-time hint — it runs in your editor and nudges you to wrap strings as you write them.This gives you fast feedback during development (ESLint in your editor) and context-aware detection in CI (Polyglot in GitHub Actions). The ESLint rule is a guardrail; Polyglot is the gate.
# Install
curl -fsSL https://getpolyglot.ai/install.sh | bash
# Detect untranslated strings (exit 1 = candidates found, 2 = scan incomplete)
polyglot scan
# Create polyglot.toml with your target languages
polyglot init --local
# See current coverage
polyglot coverage
# Run the same differential check CI runs, against your main branch
polyglot check --base origin/main
The scanner is free, requires no account, and runs locally. Add it to your CI pipeline today and stop shipping hardcoded strings.
Start in your terminal
Install the CLI, run a scan, and see exactly what you're missing. Free, no account required.