Skip to content
Back to skills

Check L10n

ASecurity

Run before pushing a PR that touched UI to find untranslated keys in any of the 21 non-English locales and to catch user-visible English strings that bypass context.l10n. Reports findings as a checklist; refuses to declare clean until all are addressed or explicitly waived. Invoke with /check-l10n.

  • 265 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 3, 2026
developmentpythongobashgitapi

Works with

  • claude code
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 3, 2026

npx -y skills add divinevideo/divine-mobile --skill check-l10n --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Check L10n?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Check L10n
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-check-l10n-divine-mobile/badge)](https://www.skillsdirectory.com/skills/divinevideo-check-l10n-divine-mobile)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: check-l10n
description: |
  Run before pushing a PR that touched UI to find untranslated keys in
  any of the 21 non-English locales and to catch user-visible English
  strings that bypass context.l10n. Reports findings as a checklist;
  refuses to declare clean until all are addressed or explicitly waived.
  Invoke with /check-l10n.
author: Claude Code
version: 1.0.0
date: 2026-05-02
user_invocable: true
invocation_hint: /check-l10n
arguments: |
  Optional: Scope to specific paths under mobile/lib/.
  Example: /check-l10n
  Example: /check-l10n mobile/lib/screens/auth
---

# Check L10n Skill

## Purpose

Catch the two ways localization breaks in this repo before users see English
in a non-English build:

1. **Untranslated keys.** A key added to `app_en.arb` but never translated
   into one of the 21 non-English locales (`am`, `ar`, `bg`, `de`, `es`,
   `fil`, `fr`, `id`, `it`, `ja`, `ko`, `ms`, `nl`, `pl`, `pt`, `ro`, `sv`,
   `tr`, `ur`, `vi`, `zh`). `mobile/lib/l10n/app_*.arb` is the live list.
2. **Hardcoded user-visible English.** Strings rendered straight to the user
   from widget code without going through `context.l10n.<key>`. These never
   show up in `.arb` files because they were never extracted, so no amount
   of translation work fixes them.

Run this before every PR push that touches `mobile/lib/`.

This skill checks that a string is *translated*. Whether it is translated in
the locale's voice is a separate question with its own source of truth:
[`mobile/docs/LOCALIZATION_STYLE_GUIDE.md`](../../../mobile/docs/LOCALIZATION_STYLE_GUIDE.md)
carries the per-locale register table, the Spanish dialect decision, the
terms that never get translated, and the review tiers.

## Workflow

### Step 1: Determine scope

If the user passed paths after `/check-l10n`, scan those. Otherwise scan the
union of staged + unstaged changes vs the working tree.

```bash
# Default: changed files
git -C mobile status --porcelain | awk '{print $NF}' | grep '\.dart$'

# Or explicit: argument paths
```

### Step 2: Run the arb consistency test (if present)

```bash
cd mobile && flutter test test/l10n/arb_consistency_test.dart
```

This test compares every `.arb` file against `app_en.arb` and fails if any
locale is missing keys that aren't on the explicit `_knownUntranslatedDebt`
allow-list.

If the test file does not exist on this branch, skip this step and rely
entirely on Step 3 plus the inline check below. Note in the report that
arb consistency was not verified.

#### Inline fallback when the test doesn't exist

If `mobile/test/l10n/arb_consistency_test.dart` is missing, do the
equivalent check by hand:

```bash
cd mobile/lib/l10n
python3 - <<'PY'
import json, glob
en = json.load(open('app_en.arb'))
en_keys = {k for k in en if not k.startswith('@') and k != '@@locale'}
for f in sorted(glob.glob('app_*.arb')):
    if f == 'app_en.arb':
        continue
    other = json.load(open(f))
    other_keys = {k for k in other if not k.startswith('@') and k != '@@locale'}
    missing = en_keys - other_keys
    if missing:
        print(f"{f}: {len(missing)} missing key(s)")
        for k in sorted(missing)[:20]:
            print(f"  - {k}")
PY
```

### Step 3: Scan for hardcoded English in changed files

```bash
python3 .agents/skills/check-l10n/scan_strings.py
```

Or with explicit paths:

```bash
python3 .agents/skills/check-l10n/scan_strings.py mobile/lib/screens/auth/foo.dart
```

The scanner emits one line per candidate, formatted
`<path>:<line>:<col>  [<rule>]  '<literal>'`. Exit code is `1` when there
are findings, `0` otherwise.

The scanner only inspects files under `mobile/lib/`. It excludes generated
files (`*.g.dart`, `*.freezed.dart`, `*.mocks.dart`), the `l10n/` directory,
and any `test/` or `integration_test/` tree. It also skips lines inside
`Log.*()`, `developer.log()`, `print()`, `assert()`, `throw <Type>Exception()`,
and route-name constants — those literals are not user-visible.

### Step 4: Report as a checklist

Output one section per category. Use the literal output of the underlying
tools rather than paraphrasing — the user should be able to copy a path and
jump straight to the line.

```
## Localization check — <branch>

### 1. ARB consistency
- ✅ All 17 locales have every key in app_en.arb
  (or)
- ❌ app_de.arb missing 7 keys: authConfirmPasswordLabel, ...
  (or)
- ⚠️  arb_consistency_test.dart not present on this branch — used inline
     fallback. Verify before merge.

### 2. Hardcoded English in changed UI files
- ✅ No likely user-visible English literals found.
  (or)
- ❌ 5 candidate(s):
  mobile/lib/screens/auth/login_options_screen.dart
    L147:26  [Text-literal]  'Amber app is not installed'
    L316:27  [label-arg]  'Sign in'
  ...
```

End with one of:

- ✅ **OK to push** — both checks passed.
- ❌ **Do not push** — list the actions required.
- ⚠️ **Push with caveat** — only after the user explicitly waives a finding,
  documenting why in the report.

## Fixing findings

### Untranslated keys

If the missing locale is one we ship to native speakers (the user can confirm
the current launch list), translate. Otherwise, add the key to the
`_knownUntranslatedDebt` set in `mobile/test/l10n/arb_consistency_test.dart`
with a comment naming which locales still need a pass. Don't expand the debt
set silently — it should always be reviewable as "the list of stuff that
isn't translated yet, on purpose".

### Hardcoded English

Each finding has three resolutions, in order of preference:

1. **Add an l10n key** to `mobile/lib/l10n/app_en.arb`, then route the
   widget through `context.l10n.<key>`. If the value already exists under a
   slightly different name, reuse it instead of creating a duplicate.
2. **Mark as not user-visible.** If the literal really isn't reaching the
   user (e.g., a debug-only widget, a developer-only flag, semantic test
   identifier), consider whether the scanner needs an additional skip line
   pattern. A skip rule should be earned by at least 3 distinct examples;
   one-offs aren't worth the regex maintenance.
3. **Waive with reason.** Brand strings and protocol tokens that should
   NEVER be translated ("Divine", "Nostr", `npub`, `nsec`, `bunker://`) are
   legitimate hardcoded English — the full locked list is in the
   [localization style guide](../../../mobile/docs/LOCALIZATION_STYLE_GUIDE.md#locked-terms--never-translated-never-transliterated).
   Note the waiver in the PR description rather than silencing the scanner —
   future readers should be able to see why this finding was accepted.

## Common rule meanings

| Rule | Catches |
|------|---------|
| `Text-literal` | `Text('Foo')` and `const Text("Bar")` |
| `AppBar-title-Text` | `title: Text('Foo')` (specialization of Text-literal) |
| `label-arg` | `label: 'Foo'` named param to any widget |
| `title-arg` | `title: 'Foo'` named param to any widget |
| `hintText-arg` | `hintText: 'Foo'` (form fields) |
| `helperText-arg` | `helperText: 'Foo'` (form fields) |
| `tooltip-arg` / `Tooltip-message` | tooltip text |
| `semanticLabel-arg` / `semanticsLabel-arg` | accessibility labels |
| `user-message-call` | first positional arg of a method whose name includes Error/Message/Snackbar/Toast/Dialog/Banner/Notification |

## Limitations

- The scanner is regex-based and will miss heavily templated code (string
  builders, `.padLeft(...)`, `'$prefix - $suffix'` constructions). Treat a
  clean run as "no obvious leaks", not "all leaks ruled out".
- Brand strings ("Divine", "OpenVine", "Vine") will sometimes trip the
  user-visible heuristic. Waive them in the PR description rather than
  trying to silence them in the scanner.
- The scanner only flags strings starting with a capital letter and
  containing a space — purely lowercase or single-word UI copy
  ("ok", "submit") will not be caught. This is a deliberate trade-off
  for signal-to-noise; manual review remains necessary for short labels.

Files in this skill

  • SKILL.md7.8 KB
  • scan_strings.py10.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.