Installs into .claude/skills of the current project.
Are you the author of 91 I18n Locale Parity Review?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/fluxonlab-91-i18n-locale-parity-review)
---
name: i18n-locale-parity-review
description: "Localization: locale parity, missing keys, i18n setup, hreflang and multilingual SEO, cultural adaptation."
---
# i18n Locale Parity Review
## Routing (absorbed skills)
Skillry 3.0.0 folded the skills below into this one. When a request names one of them or matches its topic, open the listed reference and follow it; the rest of this file stays the default procedure.
| Old skill name | Reference | Use it for |
|---|---|---|
| `i18n-agent` / `multilingual-i18n-seo` | [references/i18n-agent.md](references/i18n-agent.md) | multilingual SEO and i18n end to end: locale registry, BCP47 hreflang and x-default, sitemaps, schema inLanguage, translation playbook, CI check scripts |
| `localize-anything` | [references/localize-anything.md](references/localize-anything.md) | project localization workflow: surface discovery, glossary and translation memory, structural checks, independent language review, release evidence |
| `localization-guide` / `Localization Writer` | [references/localization-guide.md](references/localization-guide.md) | adapting copy for a target market: idioms, formality register, cultural references, dates, units, currency, reviewer notes |
## Purpose
Conduct a structured audit of every locale file in the project (JSON, PO, ARB), compare key sets across all active languages (TR/EN/DE), surface missing translations, detect hardcoded UI strings in JSX/TSX that bypass the i18n layer, and validate next-intl App Router wiring. Surface concrete evidence from actual files — never from assumptions. This skill directly supports the project's "UI Language and Theme Parity" behaviour rule: every user-facing string must exist in all active locales and be served through the i18n layer.
## When to use
- A PR adds, removes, or renames translation keys in any locale file.
- A new page, component, or modal is added and its strings may not yet be translated in all locales.
- A bug report mentions untranslated text, a missing locale fallback, or a wrong language being shown.
- The project's locale directory has grown and no parity audit has been run recently.
- You are onboarding a new locale (e.g. adding DE to an existing TR/EN project).
- The user asks to verify "UI Language and Theme Parity" compliance.
## When not to use
- The project has only one locale and internationalisation is explicitly out of scope.
- The PR is backend-only with no user-facing strings.
- A dedicated localisation platform (Phrase, Lokalise) is already running automated parity checks in CI — defer to its report unless it is stale.
## Procedure
### 1. Detect the i18n setup
```bash
# Identify the i18n library and message format
grep -r "next-intl\|react-i18next\|i18next\|vue-i18n\|@formatjs" package.json pnpm-lock.yaml 2>/dev/null | head -20
# Locate all locale/messages directories
find . -type d -name "messages" -o -type d -name "locales" -o -type d -name "i18n" \
| grep -v node_modules | grep -v ".next"
# List all locale files
find . -type f \( -name "*.json" -o -name "*.po" -o -name "*.arb" \) \
-path "*/messages/*" -o -path "*/locales/*" \
| grep -v node_modules | grep -v ".next" | sort
```
Confirm which locales are active (e.g. `en`, `tr`, `de`) and which file is the **source of truth** (typically `en.json`).
### 2. Extract and compare key sets for parity
For JSON locale files (next-intl, i18next flat/nested):
```bash
# Flatten all keys (supports nested objects) — requires Python 3
python3 - <<'EOF'
import json, sys
from pathlib import Path
def flatten(obj, prefix=""):
keys = []
for k, v in obj.items():
full = f"{prefix}.{k}" if prefix else k
if isinstance(v, dict):
keys.extend(flatten(v, full))
else:
keys.append(full)
return keys
locale_dir = Path("messages") # adjust if needed
files = {p.stem: set(flatten(json.loads(p.read_text()))) for p in locale_dir.glob("*.json")}
all_keys = set().union(*files.values())
locales = sorted(files)
print(f"{'KEY':<60} " + " ".join(f"{l:>6}" for l in locales))
for key in sorted(all_keys):
row = " ".join((" OK" if key in files[l] else "MISS") for l in locales)
if "MISS" in row:
print(f"{key:<60} {row}")
EOF
```
```bash
# Quick count per locale
for f in messages/*.json; do echo "$f: $(python3 -c "import json,sys; d=json.load(open('$f')); print(len(list(d.keys())))")"; done
```
For PO files:
```bash
# Compare msgid counts across .po files
for f in locales/**/*.po; do echo "$f: $(grep -c '^msgid ' "$f") msgids"; done
msgmerge --no-fuzzy-matching -U locales/tr/messages.po locales/en/messages.po # dry-run parity check
```
### 3. Detect hardcoded (untranslated) UI strings
```bash
# JSX/TSX text nodes that are plain strings (not wrapped in t() or <Trans>)
# Pattern: >SomeText< or >Some text with spaces< — high signal
grep -rn --include="*.tsx" --include="*.jsx" \
'>[A-Z][a-zA-Z ]\{4,\}<\|>[A-Z][a-zA-Z ]\{4,\}$' \
src/ app/ components/ \
| grep -v "//\|{t(\|<Trans\|{_\|aria-label\|placeholder\|title=" \
| head -40
# Attribute strings that should be translated (placeholders, aria-labels, titles)
grep -rn --include="*.tsx" --include="*.jsx" \
'placeholder="[A-Z][a-zA-Z ]\{3,\}"\|aria-label="[A-Z][a-zA-Z ]\{3,\}"' \
src/ app/ components/ | head -30
# Alert/toast/error messages hardcoded as English strings
grep -rn --include="*.ts" --include="*.tsx" \
'toast\.\(success\|error\|info\).*"[A-Z]\|throw new Error.*"[A-Z]\|console\.error.*"[A-Z]' \
src/ app/ | head -30
```
### 4. Validate next-intl App Router wiring
```bash
# Confirm [locale] segment exists in App Router
find app -type d -name "\[locale\]" | head -5
# Check middleware.ts exports intlMiddleware / createMiddleware
grep -n "createMiddleware\|withLocale\|intlMiddleware\|locales\s*:" middleware.ts 2>/dev/null
# Confirm next.config supports i18n or next-intl plugin
grep -n "withNextIntl\|i18n\|locales" next.config.* 2>/dev/null
# useTranslations usage in Server Components
grep -rn "useTranslations\|getTranslations" app/ --include="*.tsx" | head -20
# Check locale is passed correctly in layout
grep -n "locale\|NextIntlClientProvider\|messages" "app/[locale]/layout.tsx" 2>/dev/null \
|| grep -rn "NextIntlClientProvider" app/ --include="*.tsx" | head -10
```
### 5. Validate ICU message format, plurals, and number/date formatting
```bash
# Find keys using ICU plural/select syntax
grep -rn "plural\|select\|selectordinal" messages/ --include="*.json"
# Find number/currency/date format usages
grep -rn "useFormatter\|formatNumber\|formatDate\|Intl\.NumberFormat\|Intl\.DateTimeFormat" \
src/ app/ --include="*.tsx" --include="*.ts" | head -20
# Verify plural forms are consistent across locales (TR has 1 form, DE/EN have 2)
python3 - <<'EOF'
import json, re
from pathlib import Path
for p in Path("messages").glob("*.json"):
text = p.read_text()
plural_keys = re.findall(r'"([^"]+)":\s*"\{[^}]+,\s*plural', text)
if plural_keys:
print(f"{p.stem}: {len(plural_keys)} plural keys — {plural_keys[:5]}")
EOF
```
### 6. Check for locale-sensitive routes and redirects
```bash
# Confirm default locale redirect behaviour in middleware
grep -n "defaultLocale\|localePrefix\|pathnames" middleware.ts i18n.ts i18n/config.ts 2>/dev/null
# Look for hardcoded language in href/Link
grep -rn 'href="/en/\|href="/tr/\|href="/de/' app/ src/ --include="*.tsx" | head -20
```
## Concrete checks
- [ ] All active locale files have identical top-level and nested key sets (zero parity delta).
- [ ] No JSX/TSX file contains a user-visible plain string literal outside `t()`, `<Trans>`, or equivalent.
- [ ] `placeholder`, `aria-label`, `title`, `alt` attributes use translation keys, not raw strings.
- [ ] `toast`, `alert`, `throw new Error` messages are either translated or in a dedicated error-key namespace.
- [ ] `[locale]` dynamic segment exists in `app/` and middleware exports locale routing config.
- [ ] `NextIntlClientProvider` receives `messages` prop in the root layout.
- [ ] `useTranslations` (client) and `getTranslations` (server/async) are used correctly — not swapped.
- [ ] ICU plural keys specify correct plural forms for each locale (TR: `one/other`, DE: `one/other`, EN: `one/other`).
- [ ] `formatNumber` / `formatDate` are used instead of raw `Intl` calls in components, so locale is consistent.
- [ ] No hardcoded `/en/`, `/tr/`, `/de/` paths in `<Link>` or `router.push()`.
- [ ] `defaultLocale` is set and the middleware redirects `/` to the correct locale.
- [ ] Locale files are included in `.gitignore` exclusion for secrets but not excluded from version control themselves.
## Commands
```bash
# Full key parity delta (missing keys per locale)
python3 - <<'EOF'
import json
from pathlib import Path
def flatten(obj, prefix=""):
keys = []
for k, v in obj.items():
full = f"{prefix}.{k}" if prefix else k
if isinstance(v, dict):
keys.extend(flatten(v, full))
else:
keys.append(full)
return keys
locale_dir = Path("messages")
files = {p.stem: set(flatten(json.loads(p.read_text()))) for p in locale_dir.glob("*.json")}
all_keys = set().union(*files.values())
for locale, keys in files.items():
missing = sorted(all_keys - keys)
if missing:
print(f"\n=== {locale.upper()} MISSING {len(missing)} keys ===")
for k in missing:
print(f" {k}")
EOF
# Hardcoded string grep — save to file for review
grep -rn --include="*.tsx" --include="*.jsx" \
'>[A-Z][a-zA-Z ,!?.'"'"']\{5,\}<' \
app/ src/ components/ \
| grep -v "//\|{t(\|<Trans\|className\|data-\|import " \
> /tmp/hardcoded-strings.txt && wc -l /tmp/hardcoded-strings.txt
# Locale file sizes
wc -l messages/*.json
# next-intl version
node -e "const p=require('./node_modules/next-intl/package.json'); console.log('next-intl', p.version)"
```
## Required output
Produce a structured report with:
1. **Locale parity matrix** — table of all keys vs. all locales, marking each cell OK or MISSING. Group by namespace/section.
2. **Missing key inventory** — for each locale, the exact key paths that are absent, with the English reference value for context.
3. **Hardcoded string list** — `file:line | raw string | suggested key name`, sorted by file.
4. **i18n wiring status** — pass/fail for middleware, `[locale]` segment, `NextIntlClientProvider`, `getTranslations` vs `useTranslations` usage.
5. **ICU / format findings** — any plural or format issues per locale.
6. **Priority action** — the single most impactful fix (e.g. "Add 14 missing DE keys in auth namespace").
## Safety checks
- Never auto-delete or overwrite locale files; only report and propose additions.
- Do not add machine-translated strings directly — flag the gaps for human translators.
- When running the Python key-diff script against production locale bundles, confirm the `messages/` path is correct before running.
- Do not commit key additions unless the user explicitly approves the proposed translations.
## Completion criteria
Done means: the parity matrix covers all active locales, every missing key is listed with its English reference, every hardcoded string has a file:line entry, next-intl wiring is confirmed or flagged, and the report closes with a prioritised fix list. The "UI Language and Theme Parity" behaviour rule is satisfied when the parity delta reaches zero and no hardcoded strings remain.
Files in this skill
SKILL.md11 KB
examples/i18n-agent/astro-minimal/README.md496 B
examples/i18n-agent/astro-minimal/src/i18n/locale-registry.json688 B
examples/i18n-agent/nextjs-app-router-notes.md958 B