The Lokali GitHub Action detects Angular's src/locale/messages.{lang}.xlf XLIFF/JSON sibling layout (and any nested translation files under src/locale/messages.{lang}/), parses component $localize template tags inside the PR diff, and flags every missing entry — including ICU plural/select drift and stale @@id target-state rows left behind by an en rename.
What the Action checks for angular-i18n
Lokali reads angular.json for the configured i18n.locales[] list, walks the XLIFF/JSON sibling layout angular.json declares, and cross-references every $localize template tag in the diff to surface real coverage gaps — including ICU @@plural / @@select drift.
src/locale/messages.{lang}.xlf plus its messages.{lang}.json sibling — the layouts @angular/localize extraction emits out of the box, treated as one locale set.$localize`…` in templates or TypeScript — the Action diffs the tag IDs against messages.{lang}.xlf to find missing keys.i18n="@@id" and i18n-… attribute declarations on element templates — each with its own extracted ID that must match a translation-file entry.src/locale/messages.{lang}/… nested folders for teams that split per-feature namespaces (cart/, auth/, billing/) under a single locale root.@@plural and @@select markers in messages.xlf — the Action diffs the per-locale branch set against en so missing fr/de/ar ICU branches surface as ICU gaps, not flat missing keys.i18n.locales[] from angular.json as the source of truth for the locale set — so locales you haven't built yet are skipped, not flagged.Common coverage gaps the Action flags
These three failure modes hit real @angular/localize apps — each one lands in the comment as a per-locale missing-keys row you can act on before the PR merges.
@@id rename in en propagates through messages.xlf but a sibling locale is left with stale target-state entries@@id="cart.checkout.discount" to @@id="cart.checkout.discountBanner" in messages.en.xlf. The component template is updated in the same PR, but fr / pt-BR still carry the old <target> block under the orphan ID. The Action diff flags both rows in the sibling files (fr stale target-state, en missing-key reconciled from the new template tag) in the same PR comment.plural branches drift between en and fr / deen source defines {count, plural, one {…} other {…}} but fr/de only fill the other branch in messages.xlf. The Action diffs the per-locale ICU branch set and surfaces fr missing one, de missing one — as a structural gap, not a key-level miss.$localize key added in a new feature module without a matching messages.en.xlf entrybilling/invoice-summary.component.ts ships with $localize`:summary.exportCta:Download invoice` but the extraction step was skipped, so messages.en.xlf has no @@id="summary.exportCta". The action flags the missing id with the source component path (src/app/billing/invoice-summary.component.ts) so the gap maps back to a single PR-edit, not just a key in a translation file.Sample Action report on an angular-i18n PR
What lokali[bot] posts in the PR thread after a push touches src/locale/messages.en.xlf and src/app/billing/invoice-summary.component.ts. The same table is dropped into the first-run Coverage delta PR.
## i18n Coverage Report · angular-i18n Baseline: `src/locale/messages.en.xlf` · 218 keys · locales: 6 Package detected: `@angular/localize@17.x` · `angular.json` i18n.locales[6] | Locale | Coverage | Total | Translated | Missing | |----------|----------|-------|------------|---------| | 🟢 en | 100% | 218 | 218 | 0 | | 🟢 pt-BR | 97% | 218 | 212 | 6 | | 🟡 fr | 91% | 218 | 199 | 19 | | 🟡 de | 88% | 218 | 192 | 26 | | 🔴 ja | 69% | 218 | 151 | 67 | | 🔴 ar | 44% | 218 | 96 | 122 | Top missing keys (across all locales): • `billing.invoiceSummary.exportCta` (missing in fr, de, ja, ar, pt-BR — from src/app/billing/invoice-summary.component.ts) • `cart.checkout.discountBanner` (missing in de, ja, ar, pt-BR — @@id rename, fr has stale target-state row) • `common.placeholder.welcomeName` (placeholder drift in de) ICU plural drift per locale: • `cart.itemsCount` → en has `one`/`other`, fr has only `other` → missing `one` • `cart.itemsCount` → en has `one`/`other`, de has only `other` → missing `one` Coverage delta PR opened: #1234 — `lokali: coverage-delta-2026-08-17`
One URL works for both. Coverage comments arrive on your next PR, and the Coverage delta PR closes the gap automatically — across XLIFF/JSON siblings, $localize template tags, and ICU plural/select drift.