react-typesafe-translations
v1.3.0
Published
Co-located, type-checked translations for React components.
Maintainers
Readme
react-typesafe-translations
Sensible translations for the AI-assisted development era.
A fully type-safe internationalization library for React applications with zero build steps and minimal boilerplate. Designed for developer productivity and seamless integration with LLM coding assistants.
Features
- Full Type Safety — Every translation key, parameter, and function signature is type-checked at compile time
- Zero Build Steps — No code generation or plugins needed
- IDE Integration — Autocomplete, go-to-definition, real-time type errors
- Co-located Translations — Keep translations close to components
- AI-Assistant Friendly — Compiler errors guide LLMs to add missing translations systematically
- Automatic Code Splitting — Lazy-loaded components automatically lazy-load their translations
- Lightweight — Minimal runtime using React's
useSyncExternalStore - Function-based Interpolation — Type-safe translation functions
- Configurable Locale Rules — Mark some languages as required, others as optional
- No ICU Black Magic — Formatting and logic are handled in code, not strings
- No External Dependencies — Pure TypeScript and React, no extra libraries
- Small Bundle Size — Minimal impact on your app's size
- No missing translations — Ensures all required languages have translations defined
- Missing Key Detection — CLI and VS Code extension detect unused translation keys
Installation
npm install react-typesafe-translations
# or
yarn add react-typesafe-translations
# or
pnpm add react-typesafe-translations
# or
bun add react-typesafe-translationsQuick Start
1. Create the i18n utility
// utils/i18n.ts
import { createTranslationsFactory } from 'react-typesafe-translations';
export const i18n = createTranslationsFactory<'fi' | 'en', 'fi'>('fi');
export const setLanguage = i18n.setLanguage;
export const useLanguage = i18n.useLanguage;
export const useTranslations = i18n.useTranslations;
export type Translations = typeof i18n.Translations;2. Define translations
// translations.ts
import type { Translations } from '~/utils/i18n';
export const translations = {
welcome: {
fi: 'Tervetuloa!',
en: 'Welcome!',
},
greetUser: (name: string) => ({
fi: `Hei, ${name}!`,
en: `Hello, ${name}!`,
}),
itemCount: (count: number) => ({
fi: `${count} ${count === 1 ? 'kohde' : 'kohdetta'}`,
en: `${count} ${count === 1 ? 'item' : 'items'}`,
}),
} satisfies Translations;💡 Always use
satisfies Translations— this enables full type checking and excess key detection.
3. Use in components
import { useTranslations } from '~/utils/i18n';
import { translations } from './translations';
export function WelcomeComponent() {
const { t } = useTranslations(translations);
return (
<div>
<h1>{t.welcome}</h1>
<p>{t.greetUser('Alice')}</p>
<span>{t.itemCount(5)}</span>
</div>
);
}4. Change language
import { setLanguage } from '~/utils/i18n';
setLanguage('en');AI-Assisted Translation Workflow
One of the most powerful aspects of react-typesafe-translations is how well it works with AI coding assistants. The co-location pattern and TypeScript's type system create a perfect feedback loop for LLM-driven translation management.
Adding a New Language
When you add a new language to RequiredLanguages, TypeScript immediately flags every missing translation as a compile error:
// utils/i18n.ts
// Before: only Finnish required
export const i18n = createTranslationsFactory<'fi' | 'en', 'fi'>('fi');
// After: adding English as required
export const i18n = createTranslationsFactory<'fi' | 'en', 'fi' | 'en'>('fi');The moment you save this change, TypeScript will emit errors for every translation object missing English translations throughout your entire codebase.
LLM Agent Instructions
You can then instruct your AI assistant:
"Add English translations for all missing keys. Run the TypeScript compiler after each file to see remaining errors. Continue until all type errors are resolved."
The LLM agent will:
- Read the TypeScript errors to find files with missing translations
- Open each file and see the translation context co-located with the component
- Add appropriate English translations based on the Finnish text and component usage
- Verify the fix by checking that errors are resolved
- Move to the next file
This workflow is far superior to traditional i18n approaches where:
- Translation files are centralized and separated from usage context
- LLMs must hunt through the codebase to understand what each key means
- There's no compiler feedback to verify completeness
- Missing translations fail silently at runtime
Example: Before and After
Before (TypeScript error):
const translations = {
welcome: {
fi: 'Tervetuloa!',
// ❌ Error: Property 'en' is missing
},
} satisfies Translations;After (LLM adds translation):
const translations = {
welcome: {
fi: 'Tervetuloa!',
en: 'Welcome!', // ✅ LLM added this based on context
},
} satisfies Translations;The co-location means the LLM can see:
- The original text to translate
- The component code using the translation
- The semantic meaning from variable names and JSX context
- Any formatting functions or parameters
All in a single file, making translations accurate and contextually appropriate.
CLI and Missing Key Detection
Unused Translation Detection
The package includes a CLI tool that scans your codebase for unused translation keys:
# Check for unused translations
npx react-typesafe-translations analyze
# Specify a different directory
npx react-typesafe-translations analyze --cwd ./my-app
# Add to package.json scripts
{
"scripts": {
"lint:translations": "react-typesafe-translations analyze"
}
}The analyzer:
- Detects translation keys that are defined but never used
- Follows destructuring and variable renaming
- Tracks usage across files via imports
- Resolves shared/global wrapper hooks (e.g.
useGlobalTranslations()) back to their underlyinguseTranslations()call, so usage routed through a wrapper is still counted - Warns about dynamic access patterns that defeat type safety
- Exits with code 1 if unused keys are found (perfect for CI)
Example output:
src/components/Header.tsx:
12:3 warning Translation key 'oldTitle' is defined but never used
15:3 warning Translation key 'deprecatedLabel' is defined but never used
✖ 2 unused translation keys foundVS Code Extension
Install the companion VS Code extension for real-time feedback.
The VS Code extension provides:
- Real-time warnings for unused translation keys as you type
- Inline diagnostics directly in the editor
- Zero configuration — works automatically with
useTranslations() - Smart analysis — handles complex destructuring patterns
Make sure VS Code is using your workspace TypeScript version:
- Open Command Palette (
Cmd+Shift+P/Ctrl+Shift+P) - Run "TypeScript: Select TypeScript Version"
- Choose "Use Workspace Version"
Automatic Code Splitting
Because translations are co-located with components, you get automatic lazy loading of translations when components are lazy-loaded.
Traditional i18n: Monolithic Translation Files
In traditional i18n setups, all translations live in large JSON files:
└── locales/
├── en.json (500 KB - loads everything upfront)
└── fi.json (500 KB - loads everything upfront)Every translation for every component loads immediately, even for routes the user never visits.
react-typesafe-translations: Automatic Splitting
With co-located translations, your bundle naturally splits:
// routes.tsx
import { lazy } from 'react';
const AdminPanel = lazy(() => import('./AdminPanel'));
const UserDashboard = lazy(() => import('./UserDashboard'));When AdminPanel lazy loads, its translations load too — automatically:
// AdminPanel.tsx
import { useTranslations } from '~/utils/i18n';
const translations = {
title: { fi: 'Hallintapaneeli', en: 'Admin Panel' },
// ... more admin-specific translations
} satisfies Translations;
export function AdminPanel() {
const { t } = useTranslations(translations);
// ... component code
}Result: Users only download translations for the routes they actually visit.
Benefits
- Smaller initial bundles — Only load translations for the initial route
- Faster page loads — Reduce time-to-interactive
- No configuration needed — Works automatically with React's
lazy() - Scales naturally — As your app grows, translations split automatically
- Better caching — Each route's translations can be cached independently
This is especially impactful for large apps with many routes, where traditional i18n would load hundreds of kilobytes of unused translations upfront.
API Reference
createTranslationsFactory<AllLanguages, RequiredLanguages>(baseLanguage)
Creates the i18n factory.
AllLanguages: all allowed locales (e.g.'en' | 'fi')RequiredLanguages: subset ofAllLanguagesthat must have translations definedbaseLanguage: used as fallback, must be one ofRequiredLanguages
Returns:
useTranslations(translations)setLanguage(lang)useLanguage()Translationstype helper
Translation Objects
const translations = {
simple: {
fi: 'Hei',
en: 'Hello',
},
paramExample: {
fi: (name: string) => `Moi, ${name}`,
en: (name: string) => `Hi, ${name}`,
},
} satisfies Translations;Languages listed in RequiredLanguages must have a translation. Other languages in AllLanguages may explicitly be set to undefined to indicate intentionally missing translations. You are not allowed to omit translations completely for any language you've defined in AllLanguages.
Multiple & Shared Translation Sets
You can define separate translation groups per component or domain:
const labels = {
save: { fi: 'Tallenna', en: 'Save' },
cancel: { fi: 'Peruuta', en: 'Cancel' },
} satisfies Translations;
const labels2 = {
loading: { fi: 'Ladataan', en: 'Loading...' },
} satisfies Translations;You can use multiple useTranslations() calls in one component if needed.
Shared / global hook
For a set of translations used app-wide, you can wrap a single root object in your own hook:
// useGlobalTranslations.ts
import { translations } from './translations';
export const useGlobalTranslations = () => useTranslations(translations);// any component
const { t } = useGlobalTranslations();
return <span>{t.welcome}</span>;The unused-key analyzer understands this pattern: it resolves the wrapper hook (including wrappers that call other wrappers) back to the underlying useTranslations() call, so usage is attributed to the shared object across every component that consumes it. Renamed destructuring (const { t: tr } = useGlobalTranslations()) is supported too.
Comparison
react-typesafe-translations vs react-i18next vs typesafe-i18n
| Feature | react-typesafe-translations | react-i18next | typesafe-i18n |
| ----------------------------- | ---------- | ------------- | ------------- |
| Type-safe keys | ✅ | ❌ | ✅ |
| Function param safety | ✅ | ❌ | ✅ |
| Autocomplete translations | ✅ | ❌ | ✅ |
| Jump to definition | ✅ | ❌ | ✅ |
| satisfies-based validation | ✅ | ❌ | ❌ |
| Build step required | ❌ | ❌ | ✅ |
| External translation files | ❌ | ✅ | ✅ |
| ICU message syntax | ❌ | ✅ | ✅ |
| Per-component co-location | ✅ | ❌ | ❌ |
| LLM-friendly architecture | ✅ | ❌ | ❌ |
| Automatic code splitting | ✅ | ❌ | ❌ |
| Unused key detection | ✅ | ❌ | ❌ |
| Runtime performance | ✅ | Medium | ✅ |
| Bundle size | Minimal | Medium–Large | Small |
| Ease of setup | ✅ | ❌ | ❌ |
⚠️ While react-i18next has TypeScript types, it does not enforce key or param safety, nor provide strong IDE support out of the box.
Summary
- react-typesafe-translations is ideal for apps maintained by developers and AI assistants, with translations co-located and inline, no build tools, full TS safety, and automatic code splitting. Perfect for teams leveraging LLM coding assistants.
- react-i18next is better for content-managed apps or translator-facing tools, but lacks TS integration and requires manual bundle optimization.
- typesafe-i18n is also very type-safe, but requires codegen and centralized translation files. More scalable but less nimble. typesafe-i18n also has not been updated in a while, so it may not support the latest TypeScript features or React versions.
Why No ICU?
react-typesafe-translations does not support ICU message syntax (e.g. {count, plural, one {# item} other {# items}}) — intentionally.
ICU-based formats rely on magic string parsing and brittle runtime logic. Instead, react-typesafe-translations encourages writing actual JavaScript functions for conditionals, plurals, and logic. This keeps translations maintainable, testable, and fully type-safe.
If you prefer ICU for translator-facing tooling, consider using react-i18next or typesafe-i18n instead.
Best Practices
- Always use
satisfies Translations - Keep translations close to usage
- Keep base language values simple and complete
- Catch translation errors in CI by running typechecks
- Prefer small translation objects per component or domain
Intended Use Cases
react-typesafe-translations is ideal for:
- Small to medium React apps
- Projects with 2–5 locales
- Developer-maintained translations
- Teams using AI coding assistants (GitHub Copilot, Cursor, etc.)
- Apps that benefit from code splitting and lazy loading
- High confidence in type safety and DX
When Not to Use This
- Apps with 10+ languages (maintenance overhead grows linearly)
- Translator-facing tools (no JSON/XLIFF support)
- Apps needing dynamic runtime translations (e.g., CMS-driven)
License
MIT
