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.
| 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).
Other measured skills in the registry, with their headline benchmark lift.