ESLint rules to prevent hardcoded, user-visible strings in React/JSX and encourage proper internationalization.
This plugin provides comprehensive ESLint rules to catch hardcoded strings in JSX that should be internationalized. It helps maintain consistent i18n practices by detecting user-visible text in both JSX content and attributes.
- π JSX Text Detection: Catches hardcoded strings in JSX elements and expression containers
- π― JSX Attributes Validation: Detects hardcoded strings in accessibility and user-facing attributes
- π§ Smart Filtering: Ignores whitespace, punctuation-only content, and non-user-visible elements
- βοΈ Configurable: Opt-in rules with granular control
- π TypeScript Support: Full TypeScript compatibility
Install directly from GitHub using a commit hash:
npm install --save-dev camelohq/eslint-plugin-i18n-rules#commit-hash
# or
yarn add --dev camelohq/eslint-plugin-i18n-rules#commit-hashReplace commit-hash with the specific commit you want to use.
Add to your ESLint config:
{
"plugins": ["i18n-rules"],
"rules": {
"i18n-rules/no-hardcoded-jsx-text": "error",
"i18n-rules/no-hardcoded-jsx-attributes": "warn"
}
}Use the plugin's recommended setup, which enables no-hardcoded-jsx-text as an error and no-hardcoded-jsx-attributes as a warning:
{
"extends": ["plugin:i18n-rules/recommended"]
}module.exports = {
plugins: ["i18n-rules"],
rules: {
"i18n-rules/no-hardcoded-jsx-text": "error",
"i18n-rules/no-hardcoded-jsx-attributes": "warn", // Start with warnings
},
};Prevents hardcoded strings in JSX text content and expression containers.
<div>Hello World</div> // Direct text
<span>{"Welcome back"}</span> // String literal in expression
<p>{`Static message`}</p> // Template literal (no expressions)
<button>Save</button> // Button text<div>{t('hello.world')}</div> // i18n function
<span>{t('welcome.back')}</span> // Internationalized
<p>{`Hello ${userName}`}</p> // Dynamic template literal
<Trans>Welcome {name}</Trans> // i18n component
<div>{" "}</div> // Whitespace (ignored)
<title>Page Title</title> // HTML metadata (ignored)
<Trans i18nKey="welcome">Welcome to our app</Trans> // Trans content ignoredDetects hardcoded strings in user-visible JSX attributes that should be internationalized.
- Accessibility:
aria-label,aria-description,aria-valuetext,aria-roledescription - User-facing:
title,alt,placeholder
<button aria-label="Save document" /> // Accessibility label
<img alt="User profile picture" /> // Image alt text
<input placeholder="Enter your name" /> // Form placeholder
<div title="Click to expand" /> // Tooltip text
<div aria-description="Helpful info" /> // ARIA description<button aria-label={t('actions.save')} /> // i18n function
<img alt={t('user.profilePicture')} /> // Internationalized
<input placeholder={t('forms.enterName')} /> // Proper i18n
<div aria-labelledby="heading-id" /> // ID reference (allowed)
<div aria-describedby="description-id" /> // ID reference (allowed)
<div title="π" /> // Emoji only (ignored)
<Layout title="Page Title" /> // Wrapper component (ignored by default)
<SEO title="Welcome | My App" /> // SEO component (ignored by default){
"rules": {
"i18n-rules/no-hardcoded-jsx-attributes": [
"warn",
{
"ignoreLiterals": ["404", "N/A", "SKU-0001"],
"caseSensitive": false,
"trim": true,
"ignoreComponentsWithTitle": ["Layout", "SEO"]
}
]
}
}ignoreLiterals(string[], default:["404", "N/A"]) - Array of string literals to ignorecaseSensitive(boolean, default:false) - Case-sensitive matching for ignore literalstrim(boolean, default:true) - Trim whitespace before comparing ignore literalsignoreComponentsWithTitle(string[], default:["Layout", "SEO"]) - Components where hardcodedtitleprops are allowed
The plugin intelligently filters out content that doesn't need internationalization:
- Whitespace-only:
<div> </div>,<span>{" "}</span> - Punctuation/Symbols:
<div>β β’ β</div>,<span>{"..."}</span> - Emojis:
<div>ππ</div>,<button>{"π"}</button> - HTML Metadata:
<title>,<style>,<script>content - Trans Components: Content inside
<Trans>components from next-i18next - ID References:
aria-labelledby,aria-describedbyattributes
- Template literals with expressions:
<div>{Hello ${name}}</div> - Function calls:
<span>{formatDate(date)}</span> - Variables:
<p>{userMessage}</p>
function UserProfile({ user }) {
return (
<Layout title="User Dashboard">
{" "}
{/* Layout title allowed by default */}
<h1>User Profile</h1>
<img
src={user.avatar}
alt="Profile picture" // β Hardcoded alt text
title="Click to change avatar" // β Hardcoded tooltip
/>
<button aria-label="Edit profile">Edit</button>{" "}
{/* β Hardcoded aria-label */}
<p>Welcome back, {user.name}!</p> {/* β Hardcoded text */}
</Layout>
);
}function UserProfile({ user }) {
return (
<Layout title="User Dashboard">
{" "}
{/* β
Layout title still allowed */}
<h1>{t("profile.title")}</h1>
<img
src={user.avatar}
alt={t("profile.picture")}
title={t("profile.changeAvatar")}
/>
<button aria-label={t("actions.editProfile")}>{t("actions.edit")}</button>
<p>{t("welcome.back", { name: user.name })}</p>
</Layout>
);
}- Rule Details:
no-hardcoded-jsx-text - Rule Details:
no-hardcoded-jsx-attributes
# Install dependencies
yarn install
# Build the plugin
yarn build
# Run tests
yarn test
# Run linting
yarn lint- ESLint: ^8.0.0
- TypeScript: ~5.0.4 (for development)
- Node.js: >= 14
Contributions are welcome! Please read our contributing guidelines and ensure all tests pass before submitting a pull request.
MIT