Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scan for hardcoded UI strings, missing translation keys, RTL layout breakages, untested locales, and date/number/currency hardcoding. Use when localizing a UI, adding a locale, or auditing internationalization readiness.
.claude/skills/bilal140202-i18n-audit/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 1412% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 122% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 99% | 0% |
Find internationalization debt before the project ships into a new locale. Catches the predictable bugs: hardcoded English strings, translation keys defined in code but missing in the translation files, RTL layout assumptions baked into CSS, locale-specific formatting hardcoded as toLocaleString('en-US'), and untested locales drifting because nobody runs the app in de-DE until a customer reports it.
In:
left / right instead of logical properties(inline-start / inline-end) — likely RTL breakage.
Date, Number, Intl.DateTimeFormat, Intl.NumberFormat callswith hardcoded locale arguments other than the user's locale.
$, €, £) outside of locale-awareformatters.
Out:
audit. The audit confirms keys exist, not that translations are accurate.
the i18n-specialist verifies on a flow-by-flow basis.
ja-JP).audit scoped to the failing surface to find related issues.
tools). The audit will produce noise.
messages (logs typically aren't localized).
unless the project's i18n config covers them — the default scan patterns are JS/TS/CSS oriented.
sh paths="${PATHS:-src app packages/*/src}" locales="${LOCALES:-$(jq -r '.locales[]' i18n.config.json 2>/dev/null | tr '\n' ' ')}" source_locale="${SOURCE_LOCALE:-en}"
sh # Strings between JSX tags that aren't wrapped in t() / Trans / FormattedMessage rg -n -P '>\s*[A-Z][a-zA-Z ,.!?\047]{4,}\s*<' $paths \ | rg -v 'data-testid|aria-label|<style|<script' \ > /tmp/i18n-jsx-strings.txt
Heuristic — false positives on proper nouns ("Anthropic", "iPhone"); .claude/i18n-allow.txt lists allowed bare strings.
sh # Extract t('foo.bar') / i18n.t("foo.bar") / $t('foo.bar') keys rg -nP "\b(t|\\\$t|i18n\\.t)\\('\"'\"]" $paths -o -r '$2' \ | sort -u > /tmp/i18n-keys-used.txt
# For each locale file, list keys for f in locales/.json; do jq -r ' [paths(scalars) | map(tostring) | join(".")] | .[]' "$f" | sort -u > "/tmp/keys-${f##/}.txt" done
# used - defined-in-source-locale = missing comm -23 /tmp/i18n-keys-used.txt "/tmp/keys-${source_locale}.json.txt" \ > /tmp/i18n-missing-keys.txt
For each locale, diff its key set against the source locale's set; missing keys are translation gaps. sh for loc in $locales; do [ "$loc" = "$source_locale" ] && continue comm -23 "/tmp/keys-${source_locale}.json.txt" "/tmp/keys-${loc}.json.txt" \ > "/tmp/i18n-gap-${loc}.txt" done
sh rg -nP '\b(margin|padding|border)-(left|right)\b|\b(left|right):\s*\d' $paths \ --type css --type scss --type ts --type tsx > /tmp/i18n-rtl.txt
Findings should migrate to logical properties: margin-inline-start, padding-inline-end, inset-inline-start.
sh rg -nP '\.toLocaleString\(\s*[\'"][a-z]{2}-[A-Z]{2}[\'"]' $paths \ > /tmp/i18n-hardcoded-locale.txt rg -nP 'Intl\.(DateTimeFormat|NumberFormat)\(\s*[\'"][a-z]{2}-[A-Z]{2}[\'"]' $paths \ >> /tmp/i18n-hardcoded-locale.txt
sh rg -nP '[\$€£¥]\s*\{?[a-zA-Z0-9_]+' $paths > /tmp/i18n-currency.txt
Currency formatting belongs in Intl.NumberFormat(locale, { style: 'currency', currency: code }).
i18n.config.json / next-i18next /i18next / vue-i18n config.
playwright.config,cypress.config, .github/workflows/).
markdown # i18n audit
Locales declared: N (en, de, ja, fr-CA) Source locale: en Findings: N (hardcoded: x, missing keys: y, RTL: z, drift: w)
## Hardcoded UI strings
>Welcome back<## Missing translation keys (used in code, not in en.json)
## Locale-file drift (vs en.json)
## RTL-unsafe CSS
padding-left: 16px (use padding-inline-start)## Hardcoded locales
.toLocaleString('en-US')## Untested locales
--strict): exit 1 if hardcoded strings or missingkeys count > 0.
For a fast spot-check on one locale:
sh# Diff a locale against source jq -r 'paths(scalars) | join(".")' locales/en.json | sort > /tmp/en.keys jq -r 'paths(scalars) | join(".")' locales/de.json | sort > /tmp/de.keys diff /tmp/en.keys /tmp/de.keys
…and walk a couple of pages in the locale to eyeball layout.
inline-doc comments, and proper nouns trip the regex. The allow-list at .claude/i18n-allow.txt is meant to absorb these; expect to maintain it.
"items.count" may befine in English ({count} items) but break in Russian (3 plural forms) or Arabic (6 forms). The audit checks key existence, not plural-rule completeness — the i18n-specialist confirms with CLDR rules per flow.
Don't assume "Arabic = our only RTL test"; configure all RTL locales the project ships into.
user.name + '!' works in English, breaks in Japanese (no comma) and German (different word order). The audit can't detect this reliably — flag string-concat near t() for human review.
lang= attribute. The <html lang> should match the activelocale. Surfaces that hardcode lang="en" break screen readers in every other locale. Worth a one-line check in the audit: rg 'lang="en"' app/ src/.
en-GB → en → default. A "missing"key in en-GB may be served via fallback and look fine in QA but fail when the fallback chain is broken in production. The audit reports drift; the operator confirms whether fallback is intentional.
lib/agents/i18n-specialist.md — primary consumer.lib/skills/ux-writing-review/SKILL.md — copy quality review,which this skill complements (existence vs quality).
.claude/i18n-allow.txt (project-supplied) — allow-list for barestrings (proper nouns, codes, symbols).
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-09 | pass→pass | 7,588 | 7,948 | +5% | 1 | 1 | 0% | 1,570 | 4,057 | +158% | 0 | 0 | — |
case-10 | pass→pass | 11,908 | 6,009 | -50% | 1 | 1 | 0% | 2,289 | 3,714 | +62% | 0 | 0 | — |
case-01 | fail→fail | 16,083 | 4,716 | -71% | 1 | 1 | 0% | 3,125 | 2,878 | -8% | 0 | 0 | — |
case-02 | fail→fail | 26,993 | 6,305 | -77% | 1 | 1 | 0% | 6,220 | 3,015 | -52% | 0 | 0 | — |
case-03 | fail→fail | 25,513 | 4,005 | -84% | 1 | 1 | 0% | 5,059 | 2,730 | -46% | 0 | 0 | — |
case-04 | pass→pass | 6,132 | 12,801 | +109% | 1 | 1 | 0% | 1,130 | 4,898 | +333% | 0 | 0 | — |
case-05 | pass→fail | 12,123 | 6,129 | -49% | 1 | 1 | 0% | 2,674 | 2,710 | +1% | 0 | 0 | — |
case-06 | fail→pass | 1,816 | 11,871 | +554% | 1 | 1 | 0% | 309 | 4,671 | +1412% | 0 | 0 | — |
case-07 | pass→pass | 5,317 | 3,857 | -27% | 1 | 1 | 0% | 1,125 | 3,401 | +202% | 0 | 0 | — |
case-08 | pass→pass | 10,695 | 9,317 | -13% | 1 | 1 | 0% | 2,261 | 4,392 | +94% | 0 | 0 | — |
case-11 | pass→pass | 6,443 | 1,226 | -81% | 1 | 1 | 0% | 1,135 | 2,698 | +138% | 0 | 0 | — |
case-12 | fail→pass | 10,924 | 1,559 | -86% | 1 | 1 | 0% | 1,889 | 2,795 | +48% | 0 | 0 | — |
case-13 | fail→pass | 18,637 | 2,686 | -86% | 1 | 1 | 0% | 1,347 | 2,992 | +122% | 0 | 0 | — |
case-14 | fail→pass | 12,053 | 8,191 | -32% | 1 | 1 | 0% | 2,162 | 4,007 | +85% | 0 | 0 | — |
case-15 | pass→pass | 17,651 | 9,317 | -47% | 1 | 1 | 0% | 3,435 | 4,455 | +30% | 0 | 0 | — |
case-16 | pass→pass | 9,100 | 2,072 | -77% | 1 | 1 | 0% | 679 | 2,871 | +323% | 0 | 0 | — |
case-17 | pass→pass | 10,076 | 5,128 | -49% | 1 | 1 | 0% | 1,662 | 3,365 | +102% | 0 | 0 | — |
case-18 | pass→pass | 8,333 | 4,693 | -44% | 1 | 1 | 0% | 1,675 | 3,251 | +94% | 0 | 0 | — |
case-19 | pass→pass | 11,547 | 11,272 | -2% | 1 | 1 | 0% | 1,825 | 4,513 | +147% | 0 | 0 | — |
case-20 | pass→pass | 5,549 | 3,368 | -39% | 1 | 1 | 0% | 1,114 | 3,221 | +189% | 0 | 0 | — |
case-21 | pass→pass | 13,254 | 13,796 | +4% | 1 | 1 | 0% | 2,464 | 4,856 | +97% | 0 | 0 | — |
case-22 | fail→pass | 8,530 | 2,903 | -66% | 1 | 1 | 0% | 1,542 | 3,061 | +99% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 17 counted toward the lift figure. The other 5 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +18 percentage points is the difference between those two pass rates over the 17 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.