audit-i18n — Natural, Human-Sounding Translations
Degree of freedom: MIXED — Quality judgment and rewrite [HIGH freedom];
Phase 2 key-diff and Phase 4 locale walk [LOW freedom — run exactly].
Audit-and-fix exception. Find bad copy, then fix it. Not present-then-stop.
Translations that read like machine output erode user trust — especially in the user's own language. Find every unnatural, jargon-heavy, or technically worded string and rewrite it to sound like a real person in that locale.
The goal: every string should sound like it was written by a native speaker who understands the user, not translated from English by a developer.
Core principles for good copy
These apply to every string in every locale:
| Bad | Better | Why |
|---|---|---|
| "Invalid email format" | "That email doesn't look right — check for typos" | Guides, doesn't blame |
| "An error occurred" | "Something went wrong. Try again, or contact us if it keeps happening" | Actionable |
| "Please enter a valid value" | "This field is required" or "Enter a number between 1 and 100" | Specific |
| "Authentication failed" | "Wrong email or password" | Plain language |
| "Submit" | "Save", "Send message", "Create account" | Says what actually happens |
| "OK" | "Got it", "Done", or the specific action | Less dismissive |
| "User" | "you", "your account" | Conversational |
| "The operation was successful" | "Done!" or "Saved" | Natural |
| "Do you want to proceed?" | "Are you sure? This can't be undone." | Honest and specific |
How to reason — Observe → Interpret → Classify → Severity
- Observe — quote the source string or locale key (
file:line) and the base-locale counterpart - Interpret — does a native speaker get a next step, or a literal/dev-voiced phrase?
- Classify — hardcoded / missing key / too-literal / wrong-tone / format-mismatch / untranslated-key
- Severity — checkout, auth, and error strings first;
[TODO]if you are not a native speaker
Worked example
Observe:
CheckoutButton.tsx:42renders<button>Submit</button>(no i18n key).jaerrors.email_invalidis "無効なメールアドレス";enkey exists. Interpret: the CTA does not name the outcome; JA is a literal of "Invalid email address" — it blames and gives no next step. Classify: hardcoded + too-literal error. Severity: High — primary checkout CTA and a blocking validation error. Finding: extractbuttons.place_order; rewrite JA to guide a typo check; then apply (audit-and-fix).
Phase 0: Detect the i18n setup [HIGH freedom]
package.json → i18next, react-i18next, next-intl, vue-i18n, @angular/localize,
i18n-js, rosetta, lingui, typesafe-i18n
src/i18n/ → translation files
locales/ → JSON/YAML translation files
messages/ → next-intl message files
public/locales → common path for react-i18next
Identify:
- Library: react-i18next / next-intl / vue-i18n / lingui / custom
- Active locales: list of language codes (
en,ja,fr,de,zh, etc.) - Translation file format: JSON / YAML / TypeScript / PO files
- Completeness: are all locales present? Do non-English files have all keys?
Fetch library docs:
context7:resolve-library-id
{
"libraryName": "react-i18next"
}
Phase 1: Find hardcoded strings in source code [HIGH freedom]
Look for user-facing text that bypasses the i18n system entirely.
Scan for:
- String literals in JSX/TSX:
<p>Welcome back</p>,<button>Save</button> - Template literals with user-visible content:
`Hello ${name}` - Alert/toast calls with hardcoded strings:
toast.error("Something failed") - Error message strings in API handlers returned to the client
- Placeholder text:
placeholder="Enter your email" - Aria labels:
aria-label="Close dialog" alttext on meaningful images
Use shell to scan (the codebase search tools may be slow on large repos):
grep -rn 'placeholder="' src/ --include="*.tsx" --include="*.jsx" | head -30
grep -rn '"en":' src/ --include="*.ts" --include="*.tsx" | head -20
Build a list: file path, line number, string content, whether it has an i18n key already.
Phase 2: Audit translation file completeness [LOW freedom — run exactly]
For each locale, check that all keys from the base locale (usually en) exist:
# Example: compare en.json vs ja.json key counts
node -e "
const en = require('./public/locales/en/common.json');
const ja = require('./public/locales/ja/common.json');
const missing = Object.keys(en).filter(k => !ja[k]);
console.log('Missing in ja:', missing);
"
Or for next-intl format:
node -e "
const en = require('./messages/en.json');
const ja = require('./messages/ja.json');
function findMissing(base, target, path='') {
for (const k of Object.keys(base)) {
const kp = path ? path+'.'+k : k;
if (!(k in target)) console.log('MISSING:', kp);
else if (typeof base[k] === 'object') findMissing(base[k], target[k], kp);
}
}
findMissing(en, ja);
"
Phase 3: Review translation quality [HIGH freedom]
This is the most important phase. For each locale, review every string against the natural-language principles at the top of this skill.
What to look for in each category
Error messages — must guide, not blame, and tell the user what to do:
// Before
"errors.email_invalid": "Invalid email address"
// After
"errors.email_invalid": "That doesn't look like a valid email — check for typos and try again"
Action buttons — must describe the outcome, not just label the input:
// Before
"buttons.submit": "Submit"
"buttons.confirm": "OK"
// After
"buttons.submit": "Save changes" // or whatever the action actually does
"buttons.confirm": "Got it" // or the specific action: "Delete account", "Send message"
Empty states — should explain and invite, not just state the absence:
// Before
"empty.projects": "No projects"
// After
"empty.projects": "You haven't created any projects yet. Start one to get going."
Success messages — should feel warm and confirm the action clearly:
// Before
"success.saved": "Data saved successfully"
// After
"success.saved": "Saved!" // or "Your changes are saved"
Loading states — should be human:
// Before
"loading.data": "Loading data..."
// After
"loading.data": "Getting things ready…" // or just omit "Loading" entirely
Locale-specific tone guidelines
| Locale | Tone notes |
|---|---|
en |
Friendly, direct, conversational. Use contractions ("you've", "can't"). No formal passive voice. |
ja |
Use polite but natural keigo (〜ます/〜です form). Avoid literal translations of English idioms. Use short, clear sentences. Don't overuse katakana loanwords when Japanese alternatives exist. |
ko |
Polite 합쇼체 or 해요체 depending on the app's audience. Avoid Konglish where natural Korean exists. |
zh-TW / zh-CN |
Traditional vs simplified — do not mix. Natural, not literal. Check that date/number formats are locale-correct. |
fr |
Formal vous for most apps; tu only for clearly youth/casual products. Gender agreement must be correct — check nouns. |
de |
German compounds can get long — check for truncation. Formal Sie for business apps. |
es |
Check for tú/usted/vosotros consistency. Latin American vs Spain variants differ. |
RTL (ar, he) |
Mirror layout. Check that numbers are LTR within RTL text. Verify UI wraps correctly. |
Phase 4: Walk the live app in each locale (Playwright) [LOW freedom — run exactly]
For each active locale:
- Navigate to the locale-specific URL or trigger the locale switcher
- Walk the key flows: onboarding, core feature, error states, empty states, settings
- Screenshot each screen for visual review
// Set locale via URL or cookie depending on the app
await page.goto('http://localhost:3000/ja'); // or set cookie
await snapshot(); // capture the state
Look for:
- Strings that are still in English (untranslated keys showing as key names like
common.save) - Text that overflows its container in longer languages (German, Finnish, Russian)
- Date/time formatting that doesn't match the locale (30/01/2026 vs Jan 30, 2026 vs 2026年1月30日)
- Currency symbols in the wrong position (
$10.00vs10,00 €vs¥1,000) - Numbers using the wrong decimal/thousands separator (1,234.56 vs 1.234,56)
- Plural forms: English has singular/plural; many languages have more forms
Save screenshots under .playwright-mcp/.
Self-critique before applying [LOW freedom — do not skip]
- Evidenced —
file:line+ key, not "Japanese feels off" - Native-or-TODO — if you are not a native speaker, mark
[TODO]; do not ship confident wrong idiom - Completeness first — missing keys listed before quality nits
- Right owner — breakpoint overflow that is not locale-length →
audit-responsive; WCAG →audit-accessibility - Do not delete copy to "fix" i18n — extract or rewrite
Phase 5: Fix — priority order [HIGH freedom]
Fix 1: Extract all hardcoded strings
For every hardcoded string found in Phase 1, create an i18n key and replace it:
// Before (react-i18next)
<p>Welcome back, {name}!</p>
// After
const { t } = useTranslation('common');
<p>{t('welcome_back', { name })}</p>
// en/common.json
"welcome_back": "Welcome back, {{name}}!"
// ja/common.json
"welcome_back": "おかえりなさい、{{name}}さん!"
Fix 2: Rewrite poor translations
Edit the translation files directly. Prioritise:
- Error messages that blame or confuse users
- Empty states that dead-end users
- Action buttons that say "Submit" or "OK"
- Any string a native speaker would read and say "that's not how we talk"
Fix 3: Add missing locale keys
For any key missing in a non-English locale, add a natural translation. If you don't have a native speaker for a locale, use a clearly-marked placeholder:
"some_key": "[TODO: needs native Japanese review] {{count}} items selected"
Fix 4: Fix number/date/currency formatting
Use the i18n library's format functions rather than manual formatting:
// react-i18next with i18next-icu or i18next-intervalPlural
t('items_count', { count: 5 }) // handles plural forms automatically
// Intl API for dates and numbers (framework-agnostic)
new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(date);
new Intl.NumberFormat(locale, { style: 'currency', currency: 'JPY' }).format(1234);
Phase 6: i18n audit report
## i18n Audit Report — [App] — [Date]
### Active locales
[list with coverage % — keys present / total keys]
### Hardcoded strings found
| File | Line | String | Action |
|------|------|--------|--------|
| src/components/Foo.tsx | 42 | "Save" | Extracted to key `buttons.save` |
### Translation quality issues fixed
| Locale | Key | Before | After | Issue type |
|--------|-----|--------|-------|-----------|
| ja | errors.email_invalid | "無効なメールアドレス" | "メールアドレスが正しくないようです。入力内容をご確認ください。" | Too literal, not helpful |
### Missing keys added
[count per locale]
### Formatting fixes
[date/number/currency issues found and fixed]
### Still needs native review
[keys marked [TODO] that need a human native speaker]