audit-i18n — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited audit-i18n (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
>
Fix the strings the audit named — extract, rewrite, add the missing key — with surgical edits to the source line and the locale entry; do not reformat or re-key whole translation files, and copy you did not audit stays as it is. Unrelated bugs met on the way go in the report as follow-ups. Add tests only where the repo already tests i18n; the Phase 2 key-diff scripts stay out of the commit.
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.
| Skill | Owns |
|---|---|
| audit-i18n (this) | Audit and fix i18n: translations, locales, hardcoded strings, copy quality |
workflow-launch-ready | Launch-preparation sweep for a new app or major release |
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 |
file:line) and the base-locale counterpart[TODO] if you are not a native speakerObserve: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).
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-i18nextIdentify:
en, ja, fr, de, zh, etc.)Fetch library docs:
context7:resolve-library-id
{
"libraryName": "react-i18next"
}Look for user-facing text that bypasses the i18n system entirely.
Scan for:
<p>Welcome back</p>, <button>Save</button> Hello ${name} `toast.error("Something failed")placeholder="Enter your email"aria-label="Close dialog"alt text on meaningful imagesUse 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 -20Build a list: file path, line number, string content, whether it has an i18n key already.
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);
"This is the most important phase. For each locale, review every string against the natural-language principles at the top of this skill.
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 | 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. |
For each active locale:
PW="npx --yes @playwright/cli@latest"
$PW -s=i18n open --headed "http://localhost:3000/ja" # or set the locale cookie, then `goto`
$PW -s=i18n snapshot # capture the state
$PW -s=i18n screenshot --filename ".playwright-mcp/i18n-ja-<route>.png"Look for:
common.save)$10.00 vs 10,00 € vs ¥1,000)Save screenshots under .playwright-mcp/.
file:line + key, not "Japanese feels off"[TODO]; do not ship confident wrong idiomaudit-responsive; WCAG → audit-accessibilityFor 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}}さん!"Edit the translation files directly. Prioritise:
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"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);## 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]~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.