GitHub Action · next-intl · App Router

i18n coverage for next-intl

The Lokali GitHub Action detects your messages/{lang}.json and locales/{lang}/*.json trees, including the App Router app/[locale]/… convention and ICU MessageFormat placeholders — then posts a per-language coverage table on every PR.

messages/{lang}.json, locales/{lang}/*.json, app/[locale]/…
Scan your Next.js repo → Install the GitHub App

What the Action checks for next-intl

Lokali recognises the layouts the next-intl App Router helper ships with, and any paths you point it at via i18n/request.ts or getRequestConfig.

DETECT — messages/
Flat messages/{lang}.json file plus recursive messages/{lang}/{namespace}.json subdirectory layout used as the source of truth.
DETECT — locales/
Siblings locales/{lang}/*.json — used by older next-intl projects and teams that imported from Crowdin / Phrase exports.
DETECT — App Router locale segment
Respects the app/[locale]/… route segment and matches the matching routing.ts entries so missing locales show up before they ship.
DETECT — ICU MessageFormat
Recognises {name}, {count, plural, one {# thing} other {# things}}, and {date, date, short} placeholders and diffs them per locale for drift.
DETECT — nested namespaces
messages/{lang}/auth/login.json, messages/{lang}/billing/invoice.json — each namespace is reported independently in the PR comment.
DETECT — config-driven paths
Reads next-intl.config.js / i18n/request.ts for any localePrefix + localeDetection settings that affect which locales are in scope.

Common coverage gaps the Action flags

These three failure modes hit App Router teams most often — each one lands in the comment as both a missing-key row and the headline error message that opens the Coverage delta PR.


Sample Action report on a next-intl PR

What lokali[bot] posts in the PR thread after a push touches messages/en.json. The same table is dropped into the first-run Coverage delta PR.

lokali[bot] commented · i18n Coverage Report
## i18n Coverage Report · next-intl

Baseline: `messages/en.json` · 312 keys · locales: 8 · App Router layout

| Locale   | Coverage | Total | Translated | Missing |
|----------|----------|-------|------------|---------|
| 🟢 en    |   100%   |  312  |    312     |    0    |
| 🟢 es    |    96%   |  312  |    300     |   12    |
| 🟡 fr    |    91%   |  312  |    285     |   27    |
| 🟡 de    |    88%   |  312  |    275     |   37    |
| 🟡 ja    |    79%   |  312  |    247     |   65    |
| 🟡 pt    |    74%   |  312  |    232     |   80    |
| 🔴 zh    |    58%   |  312  |    182     |  130    |
| 🔴 ko    |    22%   |  312  |     69     |  243    |

Namespace breakdown
  • `Index` (root)         142 keys · lowest: ko (47%)
  • `Billing`              68  keys · lowest: ko (12%)  ← flagged
  • `Auth`                 54  keys · lowest: zh (39%)
  • `Settings`             48  keys · lowest: ko (8%)   ← flagged

ICU placeholder drift detected in:
  • `Billing.invoiceDue` — `de` missing `{dueDate, date, short}` token
  • `Auth.verifyEmail`   — `ja` missing `{minutes, plural, …}`

Coverage delta PR opened: #1234 — `lokali: coverage-delta-2026-08-11`

Scan a next-intl repo or install the Action

One URL works for both. Coverage comments arrive on your next PR, and the Coverage delta PR closes the gap automatically — across every namespace, including App Router route segments.

Run a free i18n audit → ← Back to integrations hub