From harness-claude
Verifies i18n compliance by detecting hardcoded strings, missing translations, locale-sensitive formatting, RTL issues, and concatenation anti-patterns across web, mobile, and backend codebases.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Internationalization compliance verification. Detect hardcoded strings, missing translations, locale-sensitive formatting, RTL issues, and concatenation anti-patterns across web, mobile, and backend codebases.
Manages i18n workflow: configures settings, scaffolds translation files, extracts strings, tracks coverage, generates pseudo-localization, retrofits projects for multi-language support.
Safely extracts and mass-converts hardcoded strings to i18n t() calls in frontend codebases using multi-pass batching, parallel language dispatch, 8 audit gates, and HTML integrity checks. For >10 strings.
Share bugs, ideas, or general feedback.
Internationalization compliance verification. Detect hardcoded strings, missing translations, locale-sensitive formatting, RTL issues, and concatenation anti-patterns across web, mobile, and backend codebases.
on_pr or on_commit triggers fire and changes touch user-facing stringson_review triggers fire to validate i18n considerationsRead harness configuration. Check harness.config.json for i18n block:
i18n.enabled -- master toggle (if false or missing, run in discovery mode: detect but do not enforce)i18n.strictness -- enforcement level (strict, standard, permissive)i18n.sourceLocale -- BCP 47 code of source language (default: en)i18n.targetLocales -- array of BCP 47 codes for target languagesi18n.framework -- auto or specific framework namei18n.platforms -- which platforms to scan (web, mobile, backend)i18n.coverage -- coverage thresholds and requirementsAuto-detect project platform(s). Scan project root for:
package.json with React/Vue/Svelte/Next/Nuxt deps, .tsx/.jsx/.vue/.svelte files*.xcodeproj, Podfile, Package.swift, .swift filesbuild.gradle/build.gradle.kts, AndroidManifest.xml, .kt/.java filespubspec.yaml with flutter dependency, .dart filespackage.json with Express/Fastify/NestJS, requirements.txt/pyproject.toml, go.mod, pom.xml/build.gradleAuto-detect i18n framework. Read framework detection profiles from agents/skills/shared/i18n-knowledge/frameworks/. For each profile, check:
detection.package_json_keys -- dependencies and devDependencies in package.jsondetection.config_files -- presence of framework config filesdetection.file_patterns -- translation file patternsLocate existing translation files. Search for:
locales/, src/locales/, public/locales/, assets/translations/.strings and .stringsdict files (iOS)res/values*/strings.xml (Android).arb files (Flutter)i18n.translationPaths is configured, use those paths instead.Load locale profiles. For each locale in i18n.targetLocales (or detected from translation files), read the locale profile from agents/skills/shared/i18n-knowledge/locales/{locale}.yaml. This provides: plural rules, text direction, expansion factor, script characteristics, common pitfalls.
Load industry profile. If i18n.industry is configured, read agents/skills/shared/i18n-knowledge/industries/{industry}.yaml for industry-specific rules.
Report detection results before proceeding:
i18n Detection Report
=====================
Config: Found (i18n.enabled: true, strictness: standard)
Platform(s): web, backend
Framework: i18next (detected from package.json)
Translation files: public/locales/{en,es,fr}/*.json (3 locales, 4 namespaces)
Source locale: en
Target locales: es, fr
Industry profile: fintech (loaded)
Determine scan scope. Based on detected platforms, select file patterns:
**/*.{tsx,jsx,vue,svelte,html} and **/*.{ts,js} (for template literals in rendering code)**/*.swift**/*.{kt,java}, **/res/values*/*.xml**/*.dart**/*.{ts,js,py,go,java} (filtered to HTTP handlers, templates, email services)node_modules, build, dist, .next, vendor, Pods, test files (unless explicitly included)Scan for hardcoded user-facing strings. For each platform, apply these detection rules:
Web (React/Vue/Svelte/vanilla):
I18N-001 String literals in JSX text content (text nodes between tags)I18N-002 String literals in i18n-sensitive props: title, placeholder, alt, aria-label, aria-descriptionI18N-003 Template literals with user-facing text in JSX or template expressionskey prop, id, data-testid, className, style, type, role, htmlFor, ref, numeric literals, boolean props, import/require pathsMobile iOS (SwiftUI/UIKit):
I18N-011 String literals in Text(), Label(), Alert(), .navigationTitle(), .toolbar labelsI18N-012 String literals in UILabel.text, UIButton.setTitle, UIAlertController messagesMobile Android (Compose/Views):
I18N-021 String literals in Text(), TextField(), Button() contentI18N-022 String literals in setText(), setTitle(), Toast.makeText()Mobile Flutter:
I18N-031 String literals in Text(), TextSpan(), AppBar(title:), SnackBar(content:)Backend:
I18N-041 String literals in HTTP response bodies (res.json({ message: '...' }), res.send('...'))I18N-042 String literals in email template content (detected via email service imports)I18N-043 String literals in notification payloadsScan for locale-sensitive formatting.
I18N-101 new Date().toLocaleDateString() without explicit locale argumentI18N-102 Number.toFixed(), .toLocaleString() without locale argumentI18N-103 Hardcoded currency symbols ($, EUR, etc.) in string templatesI18N-104 Hardcoded decimal separators (. for decimal, , for thousands)I18N-105 new Intl.DateTimeFormat() or new Intl.NumberFormat() without locale parameter (using implicit browser locale is often a bug)Scan for missing lang/dir attributes.
I18N-201 Missing lang attribute on <html> elementI18N-202 Missing dir attribute on <html> element (required when any target locale is RTL)I18N-203 Missing dir="auto" on user-generated content containers (detected via heuristic: elements rendering user input, comments, messages)I18N-204 Hardcoded left/right in CSS or style props instead of logical properties (start/end, inline-start/inline-end) -- only flagged when RTL locales are in target listScan for string concatenation.
I18N-301 String concatenation to build user-facing messages ("Hello, " + name, `Welcome ${name}` used as complete messages)I18N-302 Array .join() to build sentences or messagesI18N-303 Conditional text assembly (isPlural ? "items" : "item" -- hardcoded plural logic)t('greeting', { name }))Scan translation files for completeness.
I18N-401 Missing keys: keys present in source locale file but absent in target locale fileI18N-402 Untranslated values: values in target locale file identical to source locale (suggesting copy-paste, not translation)I18N-403 Missing plural forms: for each locale, check that all required CLDR plural categories are present. Load plural rules from locale profile (e.g., Arabic requires: zero, one, two, few, many, other).I18N-404 Empty translation values (key exists but value is empty string)I18N-405 Orphan keys: keys in translation files not referenced in source code (requires cross-referencing with source scan)Record all findings. Each finding includes:
I18N-001)strings, formatting, attributes, concatenation, translationsAssign severity based on i18n.strictness:
strict mode: all violations are error severitystandard mode: hardcoded strings and missing translations are error; formatting and concatenation are warn; orphan keys and info patterns are infopermissive mode: missing translations and hardcoded strings are warn; everything else is infoinfoGenerate summary header:
i18n Report
===========
Scanned: 87 source files, 12 translation files
Findings: 24 total (8 error, 12 warn, 4 info)
Strictness: standard
Framework: i18next
Platforms: web, backend
Locales: en (source), es, fr (targets)
List findings grouped by category. Each finding follows this format:
I18N-001 [error] Hardcoded string in JSX text content
File: src/components/Header.tsx
Line: 12
Element: <h1>Welcome to our platform</h1>
Category: strings
Fix: Wrap in translation: <h1>{t('header.welcome')}</h1>
I18N-401 [error] Missing translation key
File: public/locales/es/common.json
Key: checkout.summary.totalLabel
Source: "Total" (en)
Category: translations
Fix: Add key to es/common.json with Spanish translation
Provide category summaries with counts and severity breakdown:
Category Breakdown
------------------
Strings: 12 findings (6 error, 4 warn, 2 info)
Translations: 6 findings (4 error, 2 warn)
Formatting: 3 findings (0 error, 3 warn)
Attributes: 2 findings (1 error, 1 warn)
Concatenation: 1 finding (0 error, 1 warn)
Provide translation coverage summary (if translation files exist):
Translation Coverage
--------------------
Locale Keys Translated Coverage Missing Plurals
en 142 142 100% 0
es 142 128 90.1% 2
fr 142 135 95.1% 0
If graph is available (.harness/graph/ exists): map findings to components/routes for contextual coverage. Report per-component translation coverage.
If graph is unavailable: report per-file key-level coverage. Group findings by source file.
List actionable next steps:
This phase is optional. It applies fixes only for mechanical issues -- violations with a single, unambiguous correct fix. Translation content, key naming, and locale-specific formatting choices are never auto-fixed.
Fixable violations:
I18N-001/I18N-002: Wrap string literals in framework translation call. Detect framework from Phase 1:
{t('generated.key')} (generate key from string content, dot-notation)<FormattedMessage id="generated.key" defaultMessage="original text" />{{ $t('generated.key') }}{t('generated.key')} (generic, user picks framework later)I18N-201: Add lang="{sourceLocale}" to <html> elementI18N-202: Add dir="ltr" (or dir="auto" if RTL locales are targets) to <html> elementI18N-203: Add dir="auto" to user-content containersI18N-404: Flag empty translation values for review (not auto-fillable)Apply each fix as a minimal, targeted edit. Use the Edit tool. Do not refactor surrounding code. Do not change formatting. The fix should be the smallest possible change that resolves the violation.
Show before/after diff for each fix. Present the exact change to the user. This is a hard gate -- no fix is applied without showing the diff first.
Interactive confirmation per fix category. Group fixes by category (string wrapping, attribute addition) and ask for approval per category, not per individual fix:
Fix Category: String Wrapping (12 fixes)
-----------------------------------------
Wrap 12 hardcoded strings in t() calls across 5 files.
Generated keys follow dot-notation: component.element.description
Apply these fixes? [y/n]
Generate extraction output. For each wrapped string, output the key-value pair that needs to be added to the source locale translation file:
{
"header.welcome": "Welcome to our platform",
"checkout.totalLabel": "Total",
"auth.loginButton": "Sign in"
}
Re-scan after fixes. Run the scan phase again on fixed files to confirm violations are resolved. Report:
Do NOT fix:
harness validate -- i18n findings surface when i18n.enabled is true and i18n.strictness is strict or standard. Running validate after a scan reflects the current i18n state.harness-integrity -- The i18n scan is chained into integrity checks when i18n.enabled: true. Findings are included in the unified integrity report.harness-release-readiness -- Translation coverage is checked against i18n.coverage.minimumPercent. Per-locale coverage is reported.harness-accessibility -- When both i18n and accessibility skills are enabled, lang/dir attribute checks are handled by the i18n skill. The accessibility skill defers I18N-201/202/203 to avoid duplicate findings.harness-i18n-workflow -- After scanning, coverage gaps and extracted keys can be passed to the workflow skill for scaffolding and translation file updates.agents/skills/shared/i18n-knowledge/ -- Framework profiles, locale profiles, industry profiles, and anti-pattern catalogs are consumed during detect and scan phases.lang/dir attribute violations detected when target locales include RTL languagesharness validate reflects i18n findings at the configured strictness levelContext: A React web app using i18next with English source, targeting Spanish and French. The harness.config.json has:
{
"version": 1,
"i18n": {
"enabled": true,
"strictness": "standard",
"sourceLocale": "en",
"targetLocales": ["es", "fr"],
"framework": "auto",
"platforms": ["web"]
}
}
Phase 1: DETECT
i18n Detection Report
=====================
Config: Found (i18n.enabled: true, strictness: standard)
Platform(s): web
Framework: i18next (detected from package.json: "i18next", "react-i18next")
Translation files: public/locales/{en,es,fr}/common.json (3 locales, 1 namespace)
Source locale: en
Target locales: es, fr
Industry profile: none configured
Phase 2: SCAN
Source file with violations:
// src/components/CheckoutSummary.tsx
export function CheckoutSummary({ items, total }) {
return (
<div>
<h2>Order Summary</h2>
<p>
You have {items.length} {items.length === 1 ? 'item' : 'items'} in your cart.
</p>
<span title="Total price">Total: ${total.toFixed(2)}</span>
</div>
);
}
Findings:
I18N-001 [error] Hardcoded string in JSX text content
File: src/components/CheckoutSummary.tsx
Line: 5
Element: <h2>Order Summary</h2>
Category: strings
Fix: Wrap in translation: <h2>{t('checkout.orderSummary')}</h2>
I18N-002 [error] Hardcoded string in i18n-sensitive prop
File: src/components/CheckoutSummary.tsx
Line: 9
Element: title="Total price"
Category: strings
Fix: Wrap in translation: title={t('checkout.totalPriceTitle')}
I18N-303 [warn] Conditional text assembly (hardcoded plural logic)
File: src/components/CheckoutSummary.tsx
Line: 7
Element: items.length === 1 ? "item" : "items"
Category: concatenation
Fix: Use i18next plural: t('checkout.itemCount', { count: items.length })
I18N-103 [warn] Hardcoded currency symbol
File: src/components/CheckoutSummary.tsx
Line: 10
Element: $${total.toFixed(2)}
Category: formatting
Fix: Use Intl.NumberFormat: new Intl.NumberFormat(locale, { style: 'currency', currency }).format(total)
I18N-102 [warn] Number.toFixed() without locale-aware formatting
File: src/components/CheckoutSummary.tsx
Line: 10
Element: total.toFixed(2)
Category: formatting
Fix: Use Intl.NumberFormat for locale-aware decimal formatting
Translation file issue:
I18N-401 [error] Missing translation key
File: public/locales/es/common.json
Key: checkout.confirmButton
Source: "Confirm Order" (en)
Category: translations
Fix: Add key to es/common.json with Spanish translation
I18N-402 [warn] Untranslated value (identical to source)
File: public/locales/fr/common.json
Key: auth.welcomeMessage
Source: "Welcome back" (en)
Value: "Welcome back" (fr -- same as source, likely untranslated)
Category: translations
Fix: Translate value to French or mark as intentionally identical
Phase 3: REPORT
i18n Report
===========
Scanned: 23 source files, 6 translation files
Findings: 7 total (3 error, 3 warn, 1 info)
Strictness: standard
Framework: i18next
Platforms: web
Locales: en (source), es, fr (targets)
Category Breakdown
------------------
Strings: 2 findings (2 error, 0 warn)
Translations: 2 findings (1 error, 1 warn)
Formatting: 2 findings (0 error, 2 warn)
Concatenation: 1 finding (0 error, 1 warn)
Translation Coverage
--------------------
Locale Keys Translated Coverage Missing Plurals
en 42 42 100% 0
es 42 41 97.6% 0
fr 42 42 100% 0
(note: fr has 1 untranslated value not counted as missing)
Phase 4: FIX
Fix Category: String Wrapping (2 fixes)
-----------------------------------------
Wrap 2 hardcoded strings in t() calls in CheckoutSummary.tsx.
Generated keys: checkout.orderSummary, checkout.totalPriceTitle
Apply these fixes? [y/n]
After applying fixes:
- <h2>Order Summary</h2>
+ <h2>{t('checkout.orderSummary')}</h2>
- <span title="Total price">
+ <span title={t('checkout.totalPriceTitle')}>
Keys extracted for source locale file:
{
"checkout.orderSummary": "Order Summary",
"checkout.totalPriceTitle": "Total price"
}
Remaining violations (require human judgment): 5
| Rationalization | Reality |
|---|---|
| "This string is the app's brand name — it's technically hardcoded but obviously shouldn't be translated. I'll skip flagging it." | Brand names require explicit suppression via // i18n-ignore comment, not silent omission from the scan. Skipping without suppression means future scans have inconsistent results and the team has no record of the deliberate decision. |
| "The framework isn't in the knowledge base, but I can tell from context it's using i18next patterns — I'll apply i18next rules directly." | Unrecognized frameworks must fall back to generic detection rules, not assumed framework rules. Applying i18next-specific fix patterns to an unknown framework produces incorrect wrapping that breaks at runtime. Log the gap and use generic rules. |
"The project has i18n.enabled: false — I'll still flag errors for hardcoded strings since the team should know about them." | Respecting i18n.enabled: false is a gate. The team made a configuration decision. In that state, run in discovery mode (info severity only). Escalating to errors overrides the team's explicit choice. |
| "I18N-402 untranslated values are just warnings — I'll skip reporting them to keep the report shorter." | Untranslated values (target identical to source) are a distinct violation category with their own code. They indicate copy-paste during file creation without actual translation. Omitting them produces a misleadingly optimistic coverage report. |
| "The plural rules for this locale look complex — I'll just check for 'one' and 'other' forms like English and move on." | Plural rules are locale-specific and must be loaded from the locale profile. Arabic requires six categories; Polish requires four. Checking only English plural categories produces false-passing results for languages that require more forms. |
These are hard stops. Violating any gate means the process has broken down.
i18n.strictness specifies. If the project is in strict mode, a hardcoded string is an error. The scanner does not get to decide it is a warning.// i18n-ignore) before it is excluded from future scans.