Pre-commit i18n check. Scans changed files for untranslated strings, missing locale keys, and locale sync issues. Fast and focused — only checks what changed. Use before committing or as a review add-on.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add FriendlyInternet/nuxt-crouton --skill i18n-check --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of I18n Check?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/friendlyinternet-i18n-check)More formats (shields.io, HTML) on the badges page.
---
name: i18n-check
layer: stack
description: Pre-commit i18n check. Scans changed files for untranslated strings, missing locale keys, and locale sync issues. Fast and focused — only checks what changed. Use before committing or as a review add-on.
argument-hint: "[--scope <app> | --fix]"
allowed-tools: Bash, Read, Grep, Glob, Edit, Agent
---
# i18n Pre-Commit Check
Fast, focused translation check for changed files only. Catches the 4 most common i18n mistakes before they ship.
## Modes
| Invocation | What it checks |
|------------|---------------|
| `/i18n-check` | All uncommitted changed `.vue` files + their locale files |
| `/i18n-check --scope <app>` | Only files under `apps/<app>/` |
| `/i18n-check --fix` | Check + auto-fix all findings |
## What It Catches
### 1. Hardcoded Labels (Most Common)
Scan changed `.vue` files for user-visible text NOT wrapped in `$t()` or `t()`:
**Must be translated:**
- `label="..."` on `UFormField` (should be `:label="$t(...)"`)
- `title="..."` on `CroutonTableHeader` (should be `:title="$t(...)"`)
- `add-label="..."` on `CroutonFormRepeater` (should be `:add-label="$t(...)"`)
- `placeholder="..."` on inputs (should be `:placeholder="$t(...)"`)
- `dependent-label="..."` on dependent field loaders
- Static `{ label: 'Text', value: '...' }` arrays in select options (should be computed with `$t()`)
- Raw text in `<p>`, `<h1>`-`<h6>`, `<span>` that isn't interpolation or a component
- Button text content that isn't `{{ t(...) }}` or `{{ $t(...) }}`
- Tab/navigation labels in JS arrays (e.g., `{ label: 'Scheduling', value: 'scheduling' }`)
**Skip (not translatable):**
- CSS class strings, icon names (`i-lucide-*`)
- Route paths, collection names, field names in `name="..."` attributes
- Component names, event handlers
- Boolean/number props
- Dynamic `:label` already using `$t()` or `t()`
- Comments, `console.log()`, error messages in catch blocks
- Test files
### 2. Raw PascalCase/camelCase Titles
Detect table headers or page titles using raw collection names:
```vue
<!-- BAD: Raw collection name as title -->
title="BookingsLocations"
title="PagesPages"
<!-- GOOD: Translated or formatted -->
:title="$t('bookings.collections.locations.title')"
:title="useFormatCollections().collectionWithCapital('pagesPages')"
```
### 3. Locale Key Sync
For each app with changed locale files, or apps whose components were changed:
- Compare key structure across `en.json`, `nl.json`, `fr.json`
- Flag keys present in `en` but missing from `nl` or `fr`
- Flag values in `nl`/`fr` that are identical to `en` (likely untranslated copy-paste)
### 4. Missing Locale Infrastructure
If a changed `.vue` file uses `$t()` or `t()` but the layer has no locale files, flag it.
## Workflow
### Step 1: Gather changed files
```bash
# Get all changed .vue files (staged + unstaged)
git diff --name-only --diff-filter=ACMR HEAD | grep '\.vue$'
git diff --name-only --cached --diff-filter=ACMR | grep '\.vue$'
# Also check untracked .vue files
git ls-files --others --exclude-standard | grep '\.vue$'
```
If `--scope` is set, filter to `apps/<scope>/**`.
### Step 2: Scan templates for hardcoded text
For each changed `.vue` file, read it and check:
**Pattern matching (in `<template>` section only):**
```
# Hardcoded label props (should use :label with $t)
label="[A-Z].*" on UFormField, CroutonFormReferenceSelect, etc.
# Hardcoded title props
title="[A-Z].*" on CroutonTableHeader
# Static select option arrays in <script>
{ label: 'Text', value: '...' } (not inside computed(() => ...))
# Tab/navigation items with static labels
{ label: 'Text', value: '...' } in navigationItems or similar arrays
```
### Step 3: Check locale files
Find the locale directory for each changed component's layer:
```
apps/{app}/layers/{layer}/i18n/locales/{en,nl,fr}.json
```
Read all three and compare:
1. Flatten all keys to dot-notation
2. Find keys in `en` missing from `nl` and `fr`
3. Find values in `nl`/`fr` identical to `en` (possible untranslated)
### Step 4: Report
#### If clean:
```
## i18n Check: clean ✅
Checked N file(s), no translation issues found.
```
#### If issues found:
```
## i18n Check: N issue(s) found
### Hardcoded Labels (X)
| File | Line | Text | Suggested Key |
|------|------|------|---------------|
| `path/to/File.vue` | 43 | `label="Location"` | `bookings.fields.location` |
### Raw Collection Titles (X)
| File | Line | Current | Fix |
|------|------|---------|-----|
| `path/to/List.vue` | 34 | `title="BookingsLocations"` | `:title="$t('bookings.collections.locations.title')"` |
### Missing Locale Keys (X)
| Key | Missing From |
|-----|-------------|
| `bookings.fields.slot` | nl, fr |
### Possibly Untranslated (X)
| Key | Value (same as en) |
|-----|-------------------|
| `bookings.fields.name` | "Name" (nl), "Name" (fr) |
```
### Step 5: Auto-fix (if `--fix`)
When `--fix` is passed:
1. **Hardcoded labels** → Replace `label="Text"` with `:label="$t('suggested.key')"`
2. **Raw titles** → Replace with `$t()` or `useFormatCollections()`
3. **Static select arrays** → Wrap in `computed()` with `$t()` calls
4. **Missing locale keys** → Add to `nl.json`/`fr.json` with English placeholder
5. **Add `useI18n()` import** if needed for computed select options
After fixing, show what was changed and suggest the user verify translations in `nl.json`/`fr.json`.
## Key Naming Convention
When suggesting keys, follow this pattern:
```
{layer}.collections.{collection}.title → Collection list titles
{layer}.fields.{fieldName} → Form field labels
{layer}.actions.{actionName} → Button/action labels
{layer}.tabs.{tabName} → Tab/navigation labels
{layer}.messages.{messageName} → Helper text, toasts
{layer}.{enumGroup}.{value} → Select option labels
```
Examples:
- `bookings.fields.location` → "Location" field label
- `bookings.actions.addTimeSlot` → "Add Time Slot" button
- `bookings.triggerTypes.booking_created` → Select option
## Integration with /commit
This check runs automatically as part of `/commit` when changed files include `.vue` files in an app with i18n configured. It runs AFTER `/sync-docs` and BEFORE the actual commit.
If issues are found:
- **With `--fix`**: Fix and include fixes in the commit
- **Without `--fix`**: Report findings and ask "Fix before committing? (y/n/skip)"
## Rules
1. **Only check changed files** — never scan the whole repo (use `/i18n-audit` for that)
2. **Suggest specific keys** — don't just say "needs translation", propose the key path
3. **Don't flag internal strings** — error messages in `catch`, `console.log`, dev-only text are fine
4. **Trust existing patterns** — if a file already uses `$t()` consistently and one label is hardcoded, that's likely an oversight worth flagging
5. **Locale files are authoritative** — if a key exists in the locale file, trust it's intentionalIs this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!