skills/add-react-i18n/SKILL.md
--- name: add-react-i18n description: Add react-i18next internationalization to a React NPM library package. Creates isolated i18n instance, translation files, modifies provider and components. Use when the user wants to add multi-language support to a React component library. disable-model-invocation: true allowed-tools: Bash, Read, Write, Edit, Glob, Grep argument-hint: [default-language] [additional-languages] --- # Add react-i18next to a React NPM Library Package This skill adds internatio
npx skillsauth add landim32/awesome-ai-skills skills/add-react-i18nInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill adds internationalization (i18n) to a React NPM library package using react-i18next with an isolated i18n instance pattern that avoids conflicts with the consuming application's own i18n setup.
default-language (optional): The default language code (e.g., en, pt). Defaults to en.additional-languages (optional): Space-separated additional language codes to create translation files for (e.g., pt es fr).The implementation follows these principles:
i18next.createInstance() instead of the global instance to prevent conflicts with the consuming applanguage and translations props so consumers can override/extend translationsinitImmediate: false so translations are available immediately without async loadinguseMemo to re-evaluate when language changest parameter with English hardcoded fallbackBefore making any changes, thoroughly analyze the project structure:
Find the entry point — Read package.json to find the main/module field, then read the entry file (usually src/index.ts) to understand all public exports.
Find the Provider/Context — Search for React Context providers. Look for patterns like:
Glob: src/**/Context*.tsx, src/**/Provider*.tsx, src/contexts/**
Grep: createContext, Provider
Find all components with hardcoded strings — Search for visible text:
Grep patterns: placeholder=", label, >.*</, title=", aria-label="
Read each component and catalog all hardcoded strings (labels, placeholders, error messages, validation messages, button text, titles, etc.).
Find Zod schemas — Search for z.object, z.string(), .email(, .min(, .max( to identify validation schemas with hardcoded error messages.
Find utility functions with strings — Check src/utils/ or src/helpers/ for functions that return user-facing strings (e.g., password strength validators, formatters with error messages).
Count total strings — Estimate the total number of unique translatable strings. This helps plan the translation file structure.
npm install i18next react-i18next
Install as regular dependencies (not peer), since the library uses its own isolated instance that doesn't need to share with the consuming app.
src/
i18n/
index.ts # i18n setup + hooks
locales/
en.ts # English translations (always required)
pt.ts # Additional languages as needed
es.ts
src/i18n/locales/en.ts):const en = {
common: {
email: 'Email',
password: 'Password',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
loading: 'Loading...',
// ... shared strings used across multiple components
},
validation: {
emailInvalid: 'Please enter a valid email address',
passwordRequired: 'Password is required',
passwordMinLength: 'Password must be at least {{minLength}} characters',
// ... all validation error messages
},
// One section per component/feature:
login: {
signIn: 'Sign In',
signingIn: 'Signing in...',
rememberMe: 'Remember me',
// ...
},
register: { /* ... */ },
// etc.
};
export default en;
Key guidelines for translation files:
login.signIn, validation.emailInvalid){{variable}} syntax for interpolation (i18next standard)common.*validation.*default for clean importsCopy the English file structure exactly and translate all values. The keys must be identical.
src/i18n/index.ts)import i18next, { type Resource } from 'i18next';
import { initReactI18next, useTranslation } from 'react-i18next';
import en from './locales/en';
// import additional languages...
// Derive namespace from package name to avoid collisions
export const NAMESPACE = 'your-lib-name';
export const defaultTranslations = { en /* , pt, es, ... */ };
export function createI18nInstance(
language: string = 'en',
customTranslations?: Record<string, Record<string, unknown>>
) {
const instance = i18next.createInstance();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resources: Record<string, Record<string, any>> = {
en: { [NAMESPACE]: { ...en } },
// Add built-in languages here...
};
// Merge custom translations from consumer
if (customTranslations) {
for (const [lang, translations] of Object.entries(customTranslations)) {
if (resources[lang]) {
resources[lang][NAMESPACE] = {
...resources[lang][NAMESPACE],
...translations,
};
} else {
resources[lang] = { [NAMESPACE]: { ...translations } };
}
}
}
instance.use(initReactI18next).init({
resources: resources as Resource,
lng: language,
fallbackLng: 'en',
defaultNS: NAMESPACE,
ns: [NAMESPACE],
interpolation: { escapeValue: false },
initImmediate: false, // Synchronous init — critical for SSR and tests
});
return instance;
}
export function useLibTranslation() {
return useTranslation(NAMESPACE);
}
Critical details:
i18next.createInstance() — NOT the global i18next instanceinitImmediate: false — Ensures synchronous initializationescapeValue: false — React already escapes outputcustomTranslations parameter allows consumers to add/override translationsuseLibTranslation hook for use in componentsAdd i18n configuration to the library's config interface:
export interface LibConfig {
// ... existing config props
language?: string;
translations?: Record<string, Record<string, unknown>>;
}
In the Provider component:
import { I18nextProvider } from 'react-i18next';
import { createI18nInstance } from '../i18n';
export const LibProvider: React.FC<ProviderProps> = ({ config, children }) => {
// Create i18n instance (memoized)
const i18nInstance = useMemo(
() => createI18nInstance(config.language, config.translations),
[config.language, config.translations]
);
// Handle language changes
const currentLang = useRef(config.language);
useEffect(() => {
if (config.language && config.language !== currentLang.current) {
i18nInstance.changeLanguage(config.language);
currentLang.current = config.language;
}
}, [config.language, i18nInstance]);
return (
<I18nextProvider i18n={i18nInstance}>
<LibContext.Provider value={contextValue}>
{children}
</LibContext.Provider>
</I18nextProvider>
);
};
For each component with hardcoded strings, follow this pattern:
import { useLibTranslation } from '../i18n';
const { t } = useLibTranslation();
// Before:
<Label>Email</Label>
<Input placeholder="Enter your email" />
<Button>Sign In</Button>
// After:
<Label>{t('common.email')}</Label>
<Input placeholder={t('login.emailPlaceholder')} />
<Button>{t('login.signIn')}</Button>
Move schemas into factory functions and wrap with useMemo:
// Before (outside component):
const schema = z.object({
email: z.string().email('Please enter a valid email'),
});
// After:
function createSchema(t: (key: string) => string) {
return z.object({
email: z.string().email(t('validation.emailInvalid')),
});
}
// Inside component:
const { t } = useLibTranslation();
const schema = useMemo(() => createSchema(t), [t]);
This ensures validation messages update when the language changes.
// Translation key: "Showing {{from}} to {{to}} of {{total}}"
t('common.showingFromTo', { from: startIndex, to: endIndex, total: totalItems })
// Translation keys: "status.active", "status.inactive", etc.
const STATUS_KEYS: Record<string, string> = {
active: 'status.active',
inactive: 'status.inactive',
};
// Usage:
t(STATUS_KEYS[status])
For utility functions that return user-facing strings, add an optional t parameter:
export function validateSomething(
value: string,
options: {
// ... existing options
t?: (key: string, opts?: Record<string, unknown>) => string;
} = {}
) {
const { t } = options;
// Helper: use translation if available, otherwise hardcoded English fallback
const msg = (key: string, fallback: string, interpolation?: Record<string, unknown>) =>
t ? t(key, interpolation) : fallback;
// Usage:
feedback.push(msg('validation.minLength', `Must be at least ${min} characters`, { min }));
}
Components that call these utilities should pass { t }:
const { t } = useLibTranslation();
const result = validateSomething(value, { t });
src/index.ts)Add i18n exports to the entry point:
// i18n
export { createI18nInstance, useLibTranslation, NAMESPACE, defaultTranslations } from './i18n';
export { default as enTranslations } from './i18n/locales/en';
export { default as ptTranslations } from './i18n/locales/pt';
// ... other language exports
This allows consumers to:
Tests need the NAuthProvider (or equivalent) wrapper to initialize i18n. If tests already use the Provider wrapper, they should work without changes.
For form validation tests, use fireEvent.input + fireEvent.submit instead of fireEvent.change + fireEvent.click for more reliable Zod schema triggering in jsdom.
If tests fail because translations aren't loading, ensure:
initImmediate: false is set in the i18n init configRun these checks in order:
npm run type-check # TypeScript must pass
npm run lint # No new warnings
npm test # All tests must pass
npm run build # Build must succeed (ES + CJS)
<LibProvider config={{ apiUrl: 'https://api.example.com' }}>
<App />
</LibProvider>
<LibProvider config={{ apiUrl: 'https://api.example.com', language: 'pt' }}>
<App />
</LibProvider>
<LibProvider config={{
apiUrl: 'https://api.example.com',
language: 'es',
translations: {
es: {
common: { email: 'Correo electrónico' },
login: { signIn: 'Iniciar sesión' },
}
}
}}>
<App />
</LibProvider>
<LibProvider config={{
apiUrl: 'https://api.example.com',
translations: {
en: { login: { signIn: 'Log In' } } // overrides "Sign In"
}
}}>
<App />
</LibProvider>
Before marking complete, verify:
i18next and react-i18next installed as regular dependenciescreateInstance() (not global)initImmediate: false set in init configcommon.* and validation.* shared sections<I18nextProvider>language? and translations? propst() callsuseMemo with t dependencyt parameter with English fallbacktools
Guides how to integrate the zTools package for ChatGPT, DALL-E image generation, file upload (S3), slug generation, email sending, and document validation in a .NET 8 project. Use when the user wants to use AI features, upload files, generate slugs, send emails, or understand zTools integration.
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Create modal dialogs in the frontend using a custom Modal component built on top of Radix UI Dialog. Use this skill whenever the user asks to create, add, or modify a modal, dialog, popup, or confirmation prompt in the React application.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.