Agent skill

Audit I18n

by garfiec in garfiec/Librechat-Mobile

Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales).

MITAuto-check: notesFrontend & Design

Install Audit I18n

skills CLI
$ npx skills add garfiec/Librechat-Mobile --skill audit-i18n -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install garfiec/Librechat-Mobile audit-i18n --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/garfiec/Librechat-Mobile.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/audit-i18n .claude/skills/audit-i18n && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
audit-i18n
GitHub stars
111
Token cost
~7.2k tokens
SKILL.md length
3,536 words
Files
2
Skills in repo
5
Repo updated
First seen
Licence
MIT

At a glance

Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales).

  • Works in 3 steps: Sanity-check the tool → Run the workflow → Re-assert independently
  • Asking which features shipped English-only?
  • SKILL.md covers The two layers, and why they…, Hard rule — do not re-derive…, Phase 0 — Sanity-check the tool and Phase 1 — Run the workflow, plus 8 more sections
  • Calls python3 and git

What it does

Audit I18n is an agent skill from garfiec/Librechat-Mobile. Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales). Finds strings that exist in English but not in some or all languages, keys left as untranslated English stubs inside a translated file, stale keys the base dropped, and English literals that never reached a strings.xml at all. Use when asking "which features shipped English-only?", after landing a feature that added user-facing strings, before a release that claims multi-language support, or when a translator asks…

Its SKILL.md is about 7.2k tokens, which your agent loads only when the skill is triggered. The skill folder holds 1 other file (for example `REPORT-2026-09-30.md`).

It sits in Frontend & Design, covering Internationalization and Translation. It works with OpenAI. The repository describes itself as: Native Android & iOS client for LibreChat, built with Kotlin Multiplatform and Compose Multiplatform. The licence is MIT.

When your agent uses it

  • Asking which features shipped English-only?
  • After landing a feature that added user-facing strings
  • Before a release that claims multi-language support
  • A translator asks what still needs doing

Example prompts

  • “which features shipped English-only?”
  • “/audit-i18n”

Requirements

  • Python 3
  • Pre-approved tools (allowed-tools): Bash, Read, Glob, Grep, Write, Workflow, AskUserQuestion

Workflow steps

3 steps, taken from the step headings in SKILL.md.

  1. Sanity-check the tool
  2. Run the workflow
  3. Re-assert independently

What it can do on your machine

Read from SKILL.md and the folder at commit bf2a609. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Bash
    • Read
    • Glob
    • Grep
    • Write
    • Workflow
    • AskUserQuestion

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • python3
    • git

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use git, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Audit I18n loads about 7.2k tokens when it runs. Until then it costs about 152 tokens; SKILL.md has 3,536 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~152
When it runs · the whole SKILL.md, loaded when a task matches
~7.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NotePre-approves every shell command (allowed-tools: Bash)SKILL.md
    allowed-tools: Bash, Read, Glob, Grep, Write, Workflow, AskUserQuestion

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from garfiec/Librechat-Mobile at commit bf2a609, republished under its MIT licence (© garfiec). 3,536 words, ~7,163 tokens.

Download SKILL.mdSave it as .claude/skills/audit-i18n/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
audit-i18n
description
Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales). Finds strings that exist in English but not in some or all languages, keys left as untranslated English stubs inside a translated file, stale keys the base dropped, and English literals that never reached a strings.xml at all. Use when asking "which features shipped English-only?", after landing a feature that added user-facing strings, before a release that claims multi-language support, or when a translator asks what still needs doing. Audit-only — produces a report, never writes translations.
allowed-tools
Bash, Read, Glob, Grep, Write, Workflow, AskUserQuestion
argument-hint
[exact-only] (optional; default runs the full fan-out including heuristic triage)

Audit i18n Coverage

Audit the localization surface and report what is missing, in a form the user can act on and re-run later.

You are the lead. You do Phase 0 yourself with direct Bash, then hand the analysis to the audit-i18n workflow, then independently re-assert what it claims. You do not attribute keys to features, triage candidates, or write the report yourself — the workflow fans that out.

The deterministic checker is the source of truth. scripts/i18n-coverage.py is the only thing that produces findings. The workflow explains findings that already exist; it never discovers them.

The skill is audit-only. It ends at findings + a recommended fix order. It never writes a translation, never edits a strings.xml, never adds a Gradle task or CI gate, never commits, never opens a PR.

The two layers, and why they are separate

LayerProducesProperty
scripts/i18n-coverage.pyWhat the findings areExact, byte-deterministic, diffable across months
audit-i18n workflowWhy / when / whether it mattersJudgment, parallelised, not reproducible

Keep the seam clean. The instant an agent re-derives findings by grepping, the audit stops being comparable to the last one and the determinism you paid for is gone. Every number in the final report traces to the JSON; the workflow's contribution is feature names, dates, verdicts and priority.

Hard rule — do not re-derive findings by hand

Never grep for missing strings, diff strings.xml files with shell tools, or count keys yourself. Every number in your report must come out of the script.

The reason is not politeness, it is that a hand-rolled pass is not comparable to the next one. The script is byte-deterministic — identical input produces identical stdout on every run and every machine (no timestamps, no absolute paths, no set-iteration order). That is what makes a report diffable against the report from six weeks ago, and what lets the user prove a fix actually shrank the debt. An ad-hoc grep produces a number nobody can reproduce, and it systematically misses the things the script handles carefully: both directions of parity drift (a module with extras that cancel against its missing keys looks fine to a count comparison), values-night being a theme qualifier and not a locale, and the Android res/ surface being a separate thing that must not be folded into the parity math.

You may read source files to explain a finding the script already reported — naming the feature a key cluster belongs to, or triaging a heuristic candidate. You may not use reading to discover findings.

Phase 0 — Sanity-check the tool

Run this first, always:

bash
python3 scripts/i18n-coverage.py --summary; echo "exit=$?"

Confirm three things before trusting anything downstream:

  1. The header line reads allowlist: config/l10n/i18n-allowlist.txt. If it reads allowlist: (none) you are running unsuppressed — see the blind spot below, this failure is silent.

  2. The module table lists 10 modules and loc is 9 on every row. Fewer means discovery broke or a locale was dropped wholesale.

  3. The exit code is below 16. 16 and above is a hard failure — the script could not find or parse the surface. It shares no bits with the finding mask (1|2|4|8) so that "the check broke" can never be misread as "the code is clean". If you see it, read the failure line, fix the invocation, and re-run. Do not report anything from a hard-failed run.

    An advisory (bit 8) is not a failure and must not be treated as one. It means the tool measured something it wants a human to see — a locale directory missing wholesale, a new <plurals>, a module added since EXPECTED_MODULES was last updated. Those are findings. Carry them into the report; do not abort on them.

Phase 0 is yours and is not delegated: if the tool is broken, fanning out ten agents over its output just multiplies the wrong answer.

Phase 1 — Run the workflow

Resolve these yourself first, with Bash. The date matters: workflow scripts have no clock (Date.now() throws inside one by design, so runs stay resumable), so the report date must be computed here and passed in.

bash
REPO="$(git rev-parse --show-toplevel)"
DATE="$(date -u +%F)"
SKILL_DIR="$REPO/.claude/skills/audit-i18n"

# Never overwrite an existing dated report. Two audits on the same UTC day collide on one
# filename, and the earlier report is the baseline the later one is meant to be diffed
# against — it is uncommitted at that moment, and artifacts/ is gitignored, so an overwrite
# is unrecoverable. Pick the first free suffix instead.
REPORT_PATH="$SKILL_DIR/REPORT-$DATE.md"
n=2
while [ -e "$REPORT_PATH" ]; do
  REPORT_PATH="$SKILL_DIR/REPORT-$DATE.$n.md"
  n=$((n + 1))
done
echo "$REPO" "$DATE" "$REPORT_PATH"

Pass REPORT_PATH through verbatim — the workflow derives its artifact filenames from it, so the raw JSON behind an earlier report is preserved too. Then:

Workflow({ name: "audit-i18n", args: {
  repoRoot:     "<REPO>",
  skillDir:     "<REPO>/.claude/skills/audit-i18n",
  artifactsDir: "<REPO>/.claude/skills/audit-i18n/artifacts",
  reportPath:   "<REPORT_PATH from the snippet above — do not re-derive it>",
  reportDate:   "<DATE>",
  scope:        "full",     // or "exact-only" to skip heuristic triage
  attributeStale: true,
} })

Pass scope: "exact-only" when the user wants a fast parity-and-stubs answer, or when the heuristic candidates were triaged recently and have not moved. It drops the triage agents and leaves [H] reported as raw counts.

What it does: measures (1 agent, writes the JSON), then fans out one attribution agent per module with drift plus a stub adjudicator plus up to three heuristic triagers — all concurrently — then synthesizes one report, then reconciles that report against the raw JSON with two independent agents that did not write it. Typically 10–13 agents.

It returns { reportPath, jsonPath, totals, featureClusters, modulesAttributed, modulesNotAttributed, unreconciledModules, stubVerdictCounts, heuristicSampled, advisories, reconcileVerdicts, confirmedDiscrepanciesFixed, reconcileFailedLenses, unconfirmedDiscrepancies, clean, reportMarkdown, summary }.

No agent writes the report file — the harness refuses report-file writes from subagents. The workflow returns the reconciled report as reportMarkdown; you write it verbatim to reportPath (re-check the path is still free first) before starting Phase 2. If reportMarkdown is missing, the synthesize phase returned nothing: say so and do not hand-write a substitute.

If it returns { halted: true }, stop. That means the checker hard-failed (exit ≥ 16) — it did not run at all. Report the reason to the user and fix the tool before auditing anything. A report generated over a broken measurement reads as a clean bill of health, which is the worst possible output. Advisories alone never halt the run; they are carried into the report.

Phase 2 — Re-assert independently

Do not relay the workflow's self-report unchecked. It already reconciles itself, but it graded its own homework. Run these yourself:

Use the jsonPath the workflow returned — it is keyed to this run's report, not a fixed name.

bash
# 1. The report's headline total must equal the JSON's — the single most load-bearing number.
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('missing',d['parity']['missing_total'],'stale',d['parity']['stale_total'],'struct',d['parity']['structural_total'],'stubs',d['stubs']['counts']['errors'],'heuristic',d['hardcoded']['total'])" <jsonPath>
grep -nE "^\| \*\*total\*\*|debt|missing" <reportPath> | head

# 2. The audit is reproducible: re-running the checker reproduces the same JSON.
python3 scripts/i18n-coverage.py --format json | shasum -a 256
shasum -a 256 <jsonPath>

# 3. Spot-check one feature attribution against git yourself. --reverse|head -1 gives the
#    commit that INTRODUCED the key; `-1` alone gives the last commit that touched it, which
#    is a different (and usually wrong) answer. The name="…" anchor stops a prefix-nested
#    sibling key from matching.
git log --format='%h %ad %s' --date=short --reverse -S'name="<a key named in the report>"' \
  -- <module>/src/commonMain/composeResources/values/strings.xml | head -1

Then check the returned fields. clean: true is the only state you may summarize without caveats. When it is false, one of these is non-empty, and each means something specific:

FieldIf non-empty
modulesNotAttributedan attribution cap bit — those modules' findings are in the report but unexplained
unreconciledModulesa module's feature clusters do not account for all its keys
reconcileFailedLensesa reconcile lens returned FAIL — the report is not trustworthy as written
unconfirmedDiscrepanciesa lens suspected a fabricated or dropped finding but did not mechanically reproduce it. Not auto-fixed. Verify each by hand against the JSON before you summarize
advisoriesthe checker's own findings about the surface — these belong in your summary too

Say so to the user in your summary. Those are the parts of the surface the audit did not fully cover, and they must not be presented as if they were.

If confirmedDiscrepanciesFixed is non-zero, mention it: the report needed correcting against its own source data, which is worth knowing about the run.

Invocations

All paths are repo-relative; run from the repo root.

bash
# Full report, human-readable. The default.
python3 scripts/i18n-coverage.py

# Totals only — good for a before/after comparison or a quick status line.
python3 scripts/i18n-coverage.py --summary

# One detector at a time. Useful because the exit code then isolates that detector.
python3 scripts/i18n-coverage.py --detector parity
python3 scripts/i18n-coverage.py --detector stubs
python3 scripts/i18n-coverage.py --detector hardcoded
python3 scripts/i18n-coverage.py --detector all        # same as omitting it

# Machine-readable. Use this when you need to group, sort, or cross-tabulate findings.
python3 scripts/i18n-coverage.py --json                # shorthand for --format json
python3 scripts/i18n-coverage.py --format json
python3 scripts/i18n-coverage.py --format text         # the default

# Show the raw, unsuppressed finding set — what the allowlist is currently hiding.
python3 scripts/i18n-coverage.py --no-allowlist

# Point at a different allowlist (must resolve INSIDE the repo).
python3 scripts/i18n-coverage.py --allowlist config/l10n/i18n-allowlist.txt

# Two opt-in low-precision stub tiers, both INFO, both off by default.
python3 scripts/i18n-coverage.py --include-latin       # Latin-locale values identical to base
python3 scripts/i18n-coverage.py --near-identical      # case/punctuation-only differences

# Override repo-root detection (normally found by walking up for settings.gradle.kts).
python3 scripts/i18n-coverage.py --repo-root .

A useful pattern for grouping missing keys by feature — the highest-value analysis step — is to pull the distinct key lists out of the JSON rather than the text report:

bash
python3 scripts/i18n-coverage.py --json > /tmp/i18n.json
python3 -c "
import json
d = json.load(open('/tmp/i18n.json'))
for m in d['parity']['per_module']:
    if m['missing_total']:
        print('===', m['module'], 'base', m['base_keys'], 'rows', m['missing_total'])
        for k in m['distinct_missing_keys']: print('  ', k)
"
Exit codes

A bitmask, so a caller can tell exact findings from heuristic ones:

CodeMeaningTrust
0clean—
1parity findingsexact
2stub findings, ERROR tierexact
4hardcoded candidatesheuristic
8advisories (stale allowlist directives, module/locale-set drift, parser disagreement with StringResourceParityTest's regex)exact
16hard failure — layout unrecognized, 0 modules, a module with 0 base keys, an unreadable/non-UTF-8/unparseable resource file, bad CLI argument, a missing explicitly-passed --allowlist, unexpected exceptionthe run did not happen

Codes combine: 7 = parity + stubs + hardcoded, which is the current state of develop.

Test hard failure with exit & ~15 (equivalently exit >= 16), never with a narrower mask. The hard-failure code shares no bits with the finding mask, and that is the whole point — the conventional 70 (0b1000110) sets both the stub bit and the hardcoded bit, so exit & 3 would report a crashed run as "2 — exact stub findings". Anything at or above 16 means the check did not run; anything below is a finding set.

A caller wanting proven defects only would check exit & 3 after ruling out hard failure. Do not treat that as an invitation to add a gate — that is explicitly out of scope for this skill.

Reading the three detectors

The single most important thing you do is keep these three straight in the report. Two are exact set operations. One is a regex heuristic over a language with no type-level marking of user-facing strings. Blurring them destroys the report's credibility.

[P] parity — EXACT. Report as fact.

Set difference between each module's base values/strings.xml and each of its values-<loc>/ files, computed in both directions:

  • missing — in base, not in the locale. The user sees English in a translated app.
  • stale — in the locale, not in base. Dead weight; usually a key the base renamed or removed, which the locale files were never updated to follow.
  • structural — an entirely absent locale file (scored as 100% missing, not skipped), duplicate keys within one file, unknown locale qualifiers.

Also reported: uniform, meaning every locale in the module shares one identical missing set and one identical stale set. When uniform is yes, the per-module distinct key list is lossless and you can quote it directly. When it is no, per-locale sets diverge and you must go to the JSON parity.findings rows before making per-locale claims.

Note that missing_total is a row count summed over locales, not a key count. 504 missing rows in a 9-locale module is 56 distinct keys. State both in the report; the user sizes work in keys and sizes debt in rows.

Parity is never allowlist-suppressible. A missing or stale key is not a judgment call.

[S] stubs — EXACT. Report as fact.

A key that is present in a non-Latin-script locale but whose value is byte-identical to the English base and pure ASCII was never actually translated. The file passes parity and still ships English.

Two classes are exempted mechanically, with no allowlist entry needed: shared literals (a single token no locale anywhere translated — Bearer, OAuth, JSON, MCP, SSE, Markdown, mermaid, shadcn/ui) and letterless values (arrows, em dashes, pure format strings).

What survives is tiered by cross-locale corroboration:

  • ERROR — at least 4 other locales translated this key. The holdout is a genuine miss.
  • REVIEW — only 0–3 others did, so English may be the local convention. Report these separately and say so. 0 means multi-word English that no locale translated: usually a section every translator skipped, occasionally an example value that belongs in the allowlist. A single untranslated WORD that no locale translated (Off, Never) is still exempted as a shared literal — indistinguishable from OAuth mechanically — so only the contiguous-run corroborator can surface it.
  • INFO (--include-latin, --near-identical) — off by default and near-100% / 1-in-3 precision respectively. Only surface these if the user asks for exhaustive output, and label them INFO.

Also reported: contiguous runs of ≥3 adjacent untranslated keys in one file. These are the cheapest fixes in the whole report — one localized block of a file, usually a whole form that a translator skipped in one go. Call them out by file:first_line-last_line.

[H] hardcoded — HEURISTIC CANDIDATES. Never report as proven.

Sink-anchored regex matching for English literals in .kt that never reached a strings.xml — Text("…"), contentDescription = "…", ?: "…" error fallbacks, when branches returning display strings, and so on, bucketed into rule families.

Language rules for this section, non-negotiable:

  • Call them candidates. Never "missing strings", never "confirmed gaps", never fold their count into the translation-debt total.
  • Print the script's own precision estimate and its enumerated false-positive class verbatim rather than paraphrasing it. The script knows which of its rows are wrong; you don't.
  • Say explicitly that each one needs a human to decide whether it is user-facing at all.

Triage priority, best-value first:

  1. content_desc and text_call — unambiguously on screen, and content_desc is also an accessibility defect. Smallest families, highest hit rate.
  2. ui_param — labels/descriptions passed to UI builders. Large, but usually dominated by a few registry-shaped files where one file is one fix.
  3. when_branch and continuation — display-string mappings and multi-line concatenations.
  4. state_error and elvis_error — error fallbacks. Mixed: real user-facing error copy sits next to developer diagnostics that merely leak.
  5. enum_label and sink_call — mostly wire tokens and non-UI helpers. Verify before touching.

Sort candidates by module and by file, not by rule, when recommending work: a single file with 100 rows is one PR, and 100 rows spread over 40 files is not.

Show full SKILL.md (1,496 more words)Show less

The allowlist

config/l10n/i18n-allowlist.txt. Suppresses individual [S] and [H] findings. It does not touch [P].

Format is TAB-separated, exact string equality — no regex, no globs, no prefix matching. Read the header comment in the file for the five directive forms (literal, key, pair, site, file). It is shrink-only: a directive that suppresses nothing is surfaced as a stale advisory (exit bit 8) and must be deleted, so debt can only go down.

Every entry needs a written justification comment above it. "The check was noisy" is not one.

Legitimate reasons to add an entry:

  • The value is a protocol or wire token displayed verbatim (Basic as an HTTP auth scheme).
  • The value is a proper noun or product name (Streamable HTTP as the MCP transport's name).
  • The value is an input-format spec the user must literally type (lowercase-kebab-case).
  • The value is an example inside a placeholder, where translating it would make it wrong.
  • A [H] site is genuinely not user-facing — a wire constant, an internal exception default.

Illegitimate — this is hiding a real gap:

  • Other locales translated the same key. If ar, ja and zh rendered it as prose, ko leaving it English is a miss, not a convention. The script's REVIEW/ERROR tiering exists precisely to make this visible; do not suppress across it.
  • You could not decide, so you suppressed. Leave it as REVIEW and say it needs a native reader.
  • A whole file directive used to quiet a noisy module. Prefer site.
  • Anything to make the exit code smaller.

When the audit surfaces a suppression that looks wrong, report it as a finding against the allowlist. Do not edit the allowlist inside this skill — proposing an entry is in scope, writing it is a separate authorized change.

Report artifact

The workflow composes this, not you — its synthesize phase returns the markdown, its reconcile phase checks it against the raw JSON, and you save the returned reportMarkdown to .claude/skills/audit-i18n/REPORT-<YYYY-MM-DD>.md. This section documents the contract that report must satisfy, so you can tell whether what came back is right; the shell snippets below are what the workflow's attribution agents run.

Never overwrite a prior dated report — the whole point of determinism is that two dated reports diff cleanly. The raw JSON lands in artifacts/ (gitignored, regenerable); the dated report is the durable artifact.

Required sections:

  1. Headline number — total translation debt as a single figure (missing rows + stale rows + ERROR stub rows), so the user can size the work in one glance. Exclude [H] candidates from it and say that you did.

  2. Per-module × per-locale parity table — real numbers from the script, base key count included so percentages are checkable.

  3. Missing keys grouped by feature. This is the highest-value section. Key names are prefix-clustered by design (project_*, context_usage_*, media_*), so group them and name the feature. Then date a representative key per cluster:

    bash
    git log -S'"context_usage_label"' --format='%h|%ad|%s' --date=short --reverse \
      -- feature/chat/src/commonMain/composeResources/values/strings.xml | head -1

    and establish when localization itself landed:

    bash
    git log --oneline --reverse --diff-filter=A -- '*/values-de/strings.xml'

    Cite the commit and PR for each cluster. A cluster whose keys postdate the i18n introduction is a feature that shipped English-only — that is the user's actual question, and counts alone do not answer it.

  4. Stub findings — ERROR and REVIEW separated, per-locale counts, contiguous runs called out as the cheap wins.

  5. Hardcoded candidates — labeled heuristic, with the triage ordering above.

  6. Recommended fix order — biggest coverage win per unit of effort first, with rough entry counts per step so each step is schedulable. Deletions of stale keys and single-file registry extractions go early; they are near-zero-risk and shrink the number visibly.

Known blind spots

State these in the report. A tool whose limits are undocumented gets over-trusted.

  • No placeholder or format-specifier checking. A locale that writes %2$s where base has %1$s, or drops a %s entirely, passes every detector. This is a deliberate scope decision, not an oversight, and it is the most likely source of a runtime crash in translated copy.
  • Translation quality is entirely invisible. Non-ASCII text is accepted as translated. A machine-translated or wrong-register string is indistinguishable from a good one. The stub detector only catches English left in place.
  • The default allowlist can be absent. An explicitly-passed --allowlist that does not exist is a hard failure, but if the DEFAULT config/l10n/i18n-allowlist.txt is simply missing, the run continues unsuppressed with an ALLOWLIST_ABSENT advisory. That is a real state to notice: totals from such a run are not comparable with a run that had it. Always verify the header line reads the path, not (none).
  • --summary hides the INFO tiers. --summary --include-latin prints the same bytes as --summary alone. Use the full text or JSON output when you want those counts.
  • [H] scans .kt only, and not inside raw strings. English inside """…""" HTML/JS templates is not scanned (there is no sink pattern inside an inline web document), and android:label in AndroidManifest.xml is out of scope.
  • [H] recall limits, per the script's own notes: concatenation fragments beyond a 4-line window are reported once at the head line; a val message = "…" bound far from its sink is missed; a spaceless all-lowercase word is dropped by the prose gate (admitting them measured 2 true positives against 48 false, so that recall is given up on purpose).
  • The Android res/ surface is reported but not scored. app/src/main/res/values/strings.xml holds the platform strings and has no locale directories. Folding it into parity math would fabricate phantom "entire locale missing" failures, so it is listed separately. Its gap is real; it is just not part of the compose-resources numbers.
  • No iOS .strings / .stringsdict / .xcstrings exist, so there is nothing to compare against on that side. If any are ever added, this tool will not see them.
  • Nothing checks that a declared key is actually used. An orphaned base key that no composable references counts as real work for all 9 locales.

Relationship to StringResourceParityTest

There is an overlapping JUnit test — StringResourceParityTest plus config/l10n/strings-parity-baseline.txt (~1269 frozen entries, shrink-only) — on the unmerged local branch chore/review-loop-protocol (commit 7f4f66e8). Read it with git show chore/review-loop-protocol:<path>.

It is not on develop, so nothing currently enforces parity in CI. Do not assume its baseline reflects the present state; the checker deliberately does not read or regenerate it.

Where the two differ:

  • The test walks only shared + feature/*, so core/ui drift is invisible to it. This checker discovers modules by sorted glob and includes core/ui.
  • The test continues past a missing locale file, so a wholly absent locale is silently OK. This checker scores an absent locale file as 100% missing and reports it as a structural defect.
  • The test only checks key presence. It has no stub detection and no hardcoded-literal detection.
  • The checker emits an advisory (exit bit 8) if its parser disagrees with the test's regex, so the two can be reconciled rather than silently diverging.

Treat the checker as the superset. If the test later lands on develop, that is a CI gate decision for the user, not something this skill performs.

Anti-patterns to avoid

  • Don't grep to find findings. Re-deriving by hand produces numbers nobody can reproduce and misses both-direction drift, the values-night trap, and the res/ separation.
  • Don't merge [H] into the debt total. Heuristic candidates and exact defects are different kinds of claim. Keeping one number honest is worth more than a bigger number.
  • Don't report missing_total as a key count. It is rows summed over 9 locales. Divide, and show both.
  • Don't skip the feature attribution. "feature/chat is missing 56 keys" is not actionable. "Media gallery (#145), message queue (#167), artifact home-screen shortcuts (#241) and context usage (#204) all shipped English-only" is.
  • Don't suppress a REVIEW row to get a cleaner exit code. REVIEW exists to be escalated to a native reader, not resolved by fiat.
  • Don't write translations, edit a strings.xml, touch Gradle, or add a CI gate. All four are out of scope. The skill stops at the report.
  • Don't run Gradle or an iOS build. The deliverable is Python output and Markdown.
  • Don't report anything from a hard-failed run (exit ≥ 16). It is not a clean bill of health; the check did not run. Conversely, don't abort on an advisory (bit 8) — that is a finding, and refusing to report it is its own kind of silence.
  • Don't do the analysis yourself instead of running the workflow. Attribution across five modules is the slow part and it parallelises perfectly. Doing it inline is both slower and worse, because nothing then reconciles the report against the JSON.
  • Don't let the workflow's numbers into the summary unchecked. It reconciles itself, which is necessary but not sufficient — it graded its own homework. Phase 2 exists for that reason.
  • Don't hide a partial run. If modulesNotAttributed or unreconciledModules came back non-empty, the audit covered less than it appears to. Say which parts, plainly.
  • Don't pass a stale reportDate. The workflow cannot read a clock; whatever you pass becomes the filename and the in-report date. Compute it fresh with date -u +%F each run.

© garfiec, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in .claude/skills/audit-i18n of garfiec/Librechat-Mobile.

  • SKILL.md
  • REPORT-2026-09-30.md

Open the folder on GitHubat commit bf2a609

Compare with similar skills

Audit I18n next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Audit I18n compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Audit I18n this skillgarfiec/Librechat-Mobile111—~7.2kAutomated safety check: NotesMIT
Frontend DevelopmentOpenHands/OpenHands91k—~324Automated safety check: PassMIT
Jtackanner/jta133—~2.4kAutomated safety check: NotesMIT
Locale Tui LocalizationAyuilos/Miffan225—~1.1kAutomated safety check: PassAGPL-3.0
LobeHub Internationalizationlobehub/lobehub83k—~885Automated safety check: PassCustom licence
Chatbox i18n Translatorchatboxai/chatbox42k—~508Automated safety check: PassGPL-3.0

Similar skills

  • Frontend Development

    OpenHands/OpenHands

    This skill should be used when the user asks to "add UI copy", "add a translation", "optimize the frontend bundle", "change onboarding", "change conversation UI", "add a query key", "change MSW…

    91k GitHub stars~324 tokensUpdated today
    Frontend & DesignAuto-check passed
  • Jta

    ckanner/jta

    Translate JSON i18n files to multiple languages with AI-powered quality optimization.

    133 GitHub stars~2.4k tokensUpdated 11 mo ago
    Frontend & DesignAuto-check: notes
  • A skill your agent uses when users request i18n/localization updates for Android string resources: adding localized keys, translating strings.xml, filling missing translations, or moving hardcoded…

    225 GitHub stars~1.1k tokensUpdated today
    Frontend & DesignAuto-check passed
  • Rules for adding and editing LobeHub's user-facing strings with react-i18next: flat dot-notation keys, the default locale folder, namespaces and when to run bun run i18n.

    83k GitHub stars~885 tokensUpdated today
    Frontend & DesignAuto-check passed
  • Chatbox i18n Translator

    chatboxai/chatbox

    Translates new or changed i18n keys from a Chatbox Pro diff, staged changes or a commit range, writing the locale JSON files directly with a built-in glossary.

    42k GitHub stars~508 tokensUpdated 16 days ago
    Frontend & DesignAuto-check passed
  • Standards for keeping all user-facing text translatable: read the i18n config first, use namespaced keys, reuse shared strings and follow the key naming rules.

    33k GitHub starsUsed in 1 repo~1.9k tokens
    Frontend & DesignAuto-check passed

More from garfiec/Librechat-Mobile

  • Release Highlights

    garfiec/Librechat-Mobile

    Add a hand-written Highlights section to a GitHub release whose notes were auto-generated, summarizing the release's PRs in user-facing language above the generated changelog.

    111 GitHub stars~2k tokensUpdated yesterday
    Auto-check: notes
  • Audit Deps

    garfiec/Librechat-Mobile

    Audit open dependabot PRs in this repo. An agent skill from garfiec/Librechat-Mobile.

    111 GitHub stars~2.4k tokensUpdated yesterday
    Auto-check: notes
  • Update Web Assets

    garfiec/Librechat-Mobile

    Update the third-party JavaScript vendored into the app for the artifact, diagram, and math WebViews (KaTeX, mermaid, marked, highlight.js, Tailwind, Babel, React).

    111 GitHub stars~1.8k tokensUpdated yesterday
    Auto-check: notes
  • Sync Upstream

    garfiec/Librechat-Mobile

    Sync the Switchboard client with a newer official LibreChat server version — a stable release, a release candidate, or a PARTIAL sync up to an untagged upstream commit (e.g.

    111 GitHub stars~3.4k tokensUpdated yesterday
    Auto-check: notes

Works with

Questions about Audit I18n

What does Audit I18n do?

Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales). Audit I18n is an agent skill from garfiec/Librechat-Mobile. Audit localization / i18n coverage across the compose-resources surface (10 modules x 9 locales).

When should I use Audit I18n?

Audit I18n fits situations like: asking which features shipped English-only?; after landing a feature that added user-facing strings; before a release that claims multi-language support; A translator asks what still needs doing.

How do I install Audit I18n in Claude Code?

Run `npx skills add garfiec/Librechat-Mobile --skill audit-i18n -a claude-code`. Or copy the skill folder (.claude/skills/audit-i18n in garfiec/Librechat-Mobile) into .claude/skills/audit-i18n in your project. Claude Code loads it when a task matches its description.

How do I install Audit I18n in Codex?

Run `npx skills add garfiec/Librechat-Mobile --skill audit-i18n -a codex`. Or copy the skill folder (.claude/skills/audit-i18n in garfiec/Librechat-Mobile) into .agents/skills/audit-i18n in your project. Codex loads it when a task matches its description.

Can I use Audit I18n in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add garfiec/Librechat-Mobile --skill audit-i18n -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/audit-i18n, .gemini/skills/audit-i18n, .github/skills/audit-i18n and .opencode/skills/audit-i18n in your project.

What does Audit I18n need to run?

Going by SKILL.md and its folder, Audit I18n needs the command-line tools its instructions call (python3 and git). Our summary lists: Python 3. Its frontmatter pre-approves these tools: Bash, Read, Glob, Grep, Write, Workflow, AskUserQuestion.

Does Audit I18n access the network?

SKILL.md contains no URLs. Its commands use git, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Audit I18n safe to install?

Our automated static check of SKILL.md found notes only (pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Audit I18n use?

Audit I18n is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Audit I18n use?

About 7.2k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Audit I18n?

Skills that share tags, products or a category with Audit I18n: Frontend Development (OpenHands/OpenHands, 91k stars), Jta (ckanner/jta, 133 stars), Locale Tui Localization (Ayuilos/Miffan, 225 stars) and LobeHub Internationalization (lobehub/lobehub, 83k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Audit I18n?

garfiec (a GitHub user) maintains it in garfiec/Librechat-Mobile, which has 111 GitHub stars. The repository holds 5 skills in this directory. The repository was last updated on October 10, 2026.

Source: garfiec/Librechat-Mobile on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.