https://github.com/benkingcode/idioma
https://github.com/benkingcode/idioma
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/benkingcode/idioma
- Owner: benkingcode
- Created: 2026-03-30T20:20:12.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-05-21T11:45:36.000Z (about 2 months ago)
- Last Synced: 2026-05-21T20:00:33.901Z (about 2 months ago)
- Language: TypeScript
- Size: 1.92 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Idioma
**AI-first, compile-time i18n for React. Write in English, ship in every language.**
~800 bytes runtime. Automatic key generation. AI-powered translation that understands your codebase and key context.
Traditional i18n workflow: Write code → Extract messages → Upload to TMS → Wait for translators → Download → Test → Find bugs → Repeat.
With Idioma: Write code → Run `idioma translate` → Ship.
```bash
# Translate all missing messages with AI
idioma translate
# Extracting messages from source files...
# Generating context from source code...
# Translating 847 messages to es...
# ✓ Done in 12.3s
```
Idioma reads your source code to understand context ("checkout button", "error message in cart"), then translates with that understanding. No more uploading CSVs or waiting for translators on routine updates.
## Features
**Performance**
- **~800 bytes runtime** — Translations compile to static imports; the runtime just renders them
- **Tree-shaken bundles** — Only translations for components on each page ship to the client
- **Compile-time processing** — No runtime parsing or fetching
**Developer Experience**
- **Write natural JSX** — `Hello {name}`, not `t('greeting.hello', {name})`
- **Flexible key strategy** — Auto-generated keys for rapid development; explicit IDs when you need stable, refactor-proof translations
- **AI-powered translation** — Built-in translation with Claude or GPT, with automatic context extraction
- **Type-safe** — Generated TypeScript types message keys and required values (forget `{name}` and TypeScript tells you)
- **ICU MessageFormat** — Full support for plurals, selects, ordinals, and complex formatting
**Integrations**
- **Vite plugin** — HMR for translations, zero config
- **Next.js plugin** — Works with App Router and Pages Router
- **React Native / Metro** — First-class React Native support with Metro bundler
- **React Server Components** — Compile-time inlined translations work seamlessly in RSC
- **PO file format** — Works with Phrase, Lokalise, Crowdin, and any TMS
- **Plain JS support** — `createT` for Zod schemas, error handling, and non-React code
## Table of Contents
- [Quick Start](#quick-start) — Vite setup
- [Next.js](#nextjs)
- [React Native](#react-native)
- [React Server Components](#react-server-components)
- [Plain JavaScript](#plain-javascript-outside-react)
- [Usage](#usage)
- [Trans Component](#basic-translation)
- [useT Hook](#imperative-usage-with-uset)
- [Pluralization](#pluralization)
- [Selection](#selection)
- [Namespaces](#namespaces)
- [CLI Commands](#cli-commands)
- [Configuration](#configuration)
- [API Reference](#api-reference)
- [How It Works](#how-it-works)
- [Comparison](#comparison)
## Quick Start
```bash
npm install @idioma/core @idioma/react
```
Add the Vite plugin:
```ts
// vite.config.ts
import { idioma } from '@idioma/core/bundler/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react(), idioma()],
});
```
Create a config file:
```ts
// idioma.config.ts
import { defineConfig } from '@idioma/core';
export default defineConfig({
idiomaDir: './src/idioma',
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
});
```
This creates a folder structure:
```
src/idioma/
├── locales/ # PO files (git tracked)
├── index.ts # Trans, useT, etc. (typed wrapper)
├── plain.ts # createT (non-React)
└── .generated/ # Internal files (gitignored)
```
Set up a path alias for clean imports:
```json
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/idioma": ["./src/idioma"],
"@/idioma/*": ["./src/idioma/*"]
}
}
}
```
Set up the provider:
```tsx
// main.tsx
import { IdiomaProvider } from '@/idioma';
createRoot(document.getElementById('root')!).render(
,
);
```
Use it:
```tsx
import { Trans } from '@/idioma';
function Greeting({ name }) {
return Hello {name}!;
}
```
### Why import from generated code?
Idioma generates a thin typed wrapper in your project. This gives you full autocomplete on message IDs, locales, and namespaces without macro magic:
```ts
// Auto-generated by @idioma/core
import { createTrans, createUseT } from '@idioma/react';
import type { IdiomaTypes } from './.generated/types';
export const Trans = createTrans();
export const useT = createUseT();
```
The `IdiomaTypes` generic carries your actual message keys through to the components, so `` autocompletes with your real messages.
## Next.js
Add the Next.js plugin to your config:
```js
// next.config.mjs
import { withIdioma } from '@idioma/core/next';
export default withIdioma({
idiomaDir: './src/idioma',
defaultLocale: 'en',
})({
// your other Next.js config
});
```
Add the Babel preset:
```js
// babel.config.js
module.exports = {
presets: ['next/babel', '@idioma/core/babel-preset'],
};
```
Set up the provider in your root layout:
```tsx
// app/layout.tsx (App Router)
import { IdiomaProvider } from '@/idioma';
export default function RootLayout({ children }) {
return (
{children}
);
}
```
Works with both App Router and Pages Router.
## React Native
Add the Metro configuration wrapper:
```js
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withIdioma } = require('@idioma/core/metro');
const config = getDefaultConfig(__dirname);
module.exports = withIdioma({
idiomaDir: './src/idioma',
defaultLocale: 'en',
})(config);
```
Add the Babel preset:
```js
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['@idioma/core/babel'],
};
```
Set up the provider in your app root:
```tsx
// App.tsx
import { IdiomaProvider } from '@/idioma';
export default function App() {
return (
);
}
```
Use translations in your components:
```tsx
import { Trans, useT } from '@/idioma';
import { Text, View } from 'react-native';
function Greeting({ name }) {
const t = useT();
return (
Hello {name}!
{t('Welcome to the app')}
);
}
```
The Metro plugin automatically compiles translations on startup and watches for PO file changes during development.
> **Tip:** To use `@/idioma` imports in React Native, add [babel-plugin-module-resolver](https://github.com/tleunen/babel-plugin-module-resolver) to your Babel config.
**Note:** Idioma compiles translations at build time. For OTA translation updates without app store releases, you'll need to integrate with a service like Expo Updates or CodePush and rebuild the JS bundle.
## React Server Components
For translations in React Server Components, use `createT` from the plain module:
```tsx
// app/page.tsx (Server Component)
import { createT } from '@/idioma/plain';
export default async function Page({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = createT(locale);
return (
{t('Welcome to our app')}
{t('Hello {name}', { name: 'Ben' })}
);
}
```
The `t` function is synchronous—translations are compiled into the bundle at build time, so there's no runtime fetching. This makes RSC usage straightforward: just call `createT` with the locale and use it.
```ts
// Source text (auto-hashed to key)
t('Hello world!');
// With values (2nd arg)
t('Hello {name}', { name: 'Ben' });
// Key-only mode (like )
t({ id: 'welcome' });
t({ id: 'greeting', source: 'Hello {name}', values: { name: 'Ben' } });
```
The client-side `useT` hook uses the same API, so you can share translation patterns between server and client components.
## Plain JavaScript (Outside React)
For translations in utility functions, validation schemas, error messages, or any code outside React components, use `createT`:
```typescript
// utils/validation.ts
import { createT } from '@/idioma/plain';
export function createUserSchema(locale: string) {
const t = createT(locale);
return z.object({
email: z.string().email(t('Invalid email address')),
password: z
.string()
.min(8, t('Password must be at least {min} characters', { min: 8 })),
});
}
// Usage
const schema = createUserSchema('es');
schema.parse(formData); // Errors in Spanish
```
Works great for:
- **Zod/Yup schemas** — Localized validation messages
- **Error handling** — Translated error classes
- **Utility functions** — Any non-React code that needs i18n
- **Constants** — Lazy-evaluated translated labels
```typescript
// Custom error class
import { createT } from '@/idioma/plain';
export class AppError extends Error {
constructor(code: string, locale: string, values?: Record) {
const t = createT(locale);
super(t({ id: `errors.${code}`, values }));
this.name = 'AppError';
}
}
// API response helper
export function formatApiError(code: string, locale: string) {
const t = createT(locale);
return { success: false, message: t(`error.${code}`) };
}
```
**Bundle splitting:** In production, Babel inlines translations at each call site. Static strings (`t('literal')`) get optimal tree-shaking. Dynamic strings (`t(variable)`) trigger an automatic translations import for runtime lookup—Babel injects this only in files that need it.
## Usage
### Basic translation
```tsx
Welcome to our app
```
### Variable interpolation
```tsx
You have {count} new messages
```
### Component interpolation
```tsx
Read our terms of service and{' '}
privacy policy
```
### Explicit keys
Use explicit IDs for translations that need to survive source text refactoring:
```tsx
// ID-only (translation comes entirely from PO file)
// Hybrid: stable ID + fallback content if translation is missing
Confirm Order
```
This is useful for checkout flows, legal text, or any message that goes through formal translation review. See [Choosing Your Key Strategy](#choosing-your-key-strategy) for guidance.
### Context
Add translator context that affects key generation (same text, different context = different key):
```tsx
Submit
Submit
```
### Pluralization
Use the `plural()` function inside `` or `t()` for pluralization:
```tsx
import { Trans } from '@/idioma';
import { plural } from '@idioma/core/icu';
// In Trans component
You have {plural(count, { one: '# item', other: '# items' })} in your cart
;
// In t() template literal
const t = useT();
t(`You have ${plural(count, { one: '# item', other: '# items' })} in cart`);
```
The `#` placeholder is replaced with the numeric value. Both usages compile to ICU MessageFormat:
```
You have {count, plural, one {# item} other {# items}} in cart
```
Full plural forms for different languages:
```tsx
plural(count, {
zero: 'No items', // 0 items (some languages)
one: '# item', // 1 item
two: '# items', // 2 items (Arabic, Welsh)
few: '# items', // 2-4 items (Slavic languages)
many: '# items', // 5+ items (Slavic, Arabic)
other: '# items', // Required fallback
});
```
### Selection
Use the `select()` function for exact value matching (gender, status, categories):
```tsx
import { Trans } from '@/idioma';
import { select } from '@idioma/core/icu';
// In Trans component
{select(gender, { male: 'He', female: 'She', other: 'They' })} liked your post
;
// In t() template literal
const t = useT();
t(
`${select(status, { pending: 'Waiting', approved: 'Accepted', other: 'Unknown' })}`,
);
```
This compiles to ICU MessageFormat:
```
{gender, select, male {He} female {She} other {They}} liked your post
```
The `other` form is required as a fallback for unmatched values.
### Ordinal Numbers
Use `selectOrdinal()` for ordinal formatting (1st, 2nd, 3rd):
```tsx
import { Trans } from '@/idioma';
import { selectOrdinal } from '@idioma/core/icu';
// In Trans component
You finished in{' '}
{selectOrdinal(place, { one: '#st', two: '#nd', few: '#rd', other: '#th' })}{' '}
place
;
// In t() template literal
const t = useT();
t(
`Your ${selectOrdinal(attempt, { one: '#st', two: '#nd', few: '#rd', other: '#th' })} attempt`,
);
```
The `#` placeholder is replaced with the numeric value. This compiles to ICU MessageFormat:
```
You finished in {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place
```
Ordinal rules are locale-aware via CLDR. In English:
- `one` = 1, 21, 31... (ends in 1, not 11)
- `two` = 2, 22, 32... (ends in 2, not 12)
- `few` = 3, 23, 33... (ends in 3, not 13)
- `other` = 4, 5, 11, 12, 13, 14...
### Number and Date Formatting
For number, currency, and date formatting, use the standard `Intl` APIs alongside Idioma:
```tsx
import { Trans, useLocale } from '@/idioma';
function Price({ amount }) {
const locale = useLocale();
const formatted = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'EUR',
}).format(amount);
return Total: {formatted};
}
function EventDate({ date }) {
const locale = useLocale();
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
}).format(date);
return Event on {formatted};
}
```
Idioma focuses on message translation and delegates formatting to the platform's `Intl` APIs, which handle locale-specific number, currency, and date formatting natively.
### Imperative usage with useT
```tsx
import { useT } from '@/idioma';
function SearchInput() {
const t = useT();
return ;
}
function Greeting({ name }) {
const t = useT();
// With values (2nd arg)
const greeting = t('Hello {name}', { name });
// With context (3rd arg - changes hash)
const submitLabel = t('Submit', undefined, { context: 'button' });
// With both values and context
const message = t('Welcome {user}', { user: name }, { context: 'header' });
return {greeting};
}
```
### useLocale
Get the current locale:
```tsx
import { useLocale } from '@/idioma';
function LanguageSwitcher() {
const locale = useLocale();
return Current: {locale};
}
```
### Namespaces
Organize translations into namespaces for large apps:
```tsx
// In components
Welcome to our platform;
// With useT
const t = useT();
t('Subscribe now', undefined, { ns: 'marketing' });
```
## CLI Commands
Run via npx or your package manager:
```bash
npx idioma
pnpm idioma
```
### extract
Extract messages from source files to PO:
```bash
idioma extract # Extract all messages
idioma extract --clean # Remove unused messages
idioma extract --watch # Watch for changes
```
### compile
Compile PO files to JavaScript:
```bash
idioma compile
```
> **Note:** The Vite, Next.js, and Metro plugins run `compile` automatically during build. You only need to run this manually if you're using a different bundler or want to inspect the generated output.
### translate
AI-powered translation (requires `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`):
```bash
idioma translate # Translate missing messages
idioma translate --force # Retranslate all messages
idioma translate --dry-run # Run without API calls or saving
idioma translate --verbose # Show AI prompts and responses
idioma translate --no-auto-context # Skip automatic context generation
```
**How it works:**
1. **Guidelines:** Configure `ai.guidelines` to describe your app's tone, audience, and style. These guidelines are sent to the AI during both context generation and translation, ensuring consistent results across your entire app.
2. **Context generation:** Idioma reads your source files and uses AI to generate translator context for each message (e.g., "Button label in checkout form", "Error shown when payment fails"). This context is saved to the PO file.
3. **Translation:** The AI translates each message using the generated context and your project guidelines, producing more accurate results than context-free translation.
```po
#. [AI Context]: Button label shown when user confirms their order
#: src/components/Checkout.tsx:42
msgid "Confirm"
msgstr "Confirmar"
```
**Cost and performance:** Translation uses batched API calls. A typical 1,000-message project costs roughly $0.50–2.00 USD depending on message complexity and provider. Context generation is a one-time cost per message; subsequent translations reuse existing context.
**Dry run mode:** The `--dry-run` flag runs the translation pipeline without making API calls or saving files. Combined with `--verbose`, you can inspect the exact prompts that would be sent to the AI—useful for debugging your guidelines or understanding what context each message receives:
```bash
idioma translate --dry-run --verbose
```
**Review workflow:** Translations are written to standard PO files that can be committed and reviewed in your normal PR process, or synced to a TMS like Phrase, Lokalise, or Crowdin for professional review.
### check
Validate translation completeness:
```bash
idioma check # Check all locales
idioma check --locale es # Check specific locale
```
Exits with code 1 if translations are incomplete.
### stats
Show translation statistics:
```bash
idioma stats
```
## Configuration
```ts
// idioma.config.ts
import { defineConfig } from '@idioma/core';
export default defineConfig({
// Base directory for all Idioma files
// Generated files go in {idiomaDir}/, PO files in {idiomaDir}/locales/ by default
idiomaDir: './src/idioma',
// Optional: Override PO file location if you have existing translations elsewhere
// localesDir: './locales',
// Source language
defaultLocale: 'en',
// Target languages
locales: ['en', 'es', 'fr', 'de', 'ja'],
// Enable Suspense-based lazy loading (React 19+)
useSuspense: false,
// Files to scan for messages (default: ['**/*.tsx', '**/*.jsx', '**/*.ts', '**/*.js'])
sourcePatterns: ['src/**/*.tsx', 'src/**/*.jsx'],
// AI translation config (uses Vercel AI SDK)
// Install your preferred provider: pnpm add @ai-sdk/anthropic
ai: {
model: anthropic('claude-sonnet-4-5'), // import { anthropic } from '@ai-sdk/anthropic'
// See https://ai-sdk.dev/providers for other providers (openai, google, etc.)
// Project-specific guidelines for AI translation
guidelines: `This is a children's educational game for ages 4-8.
Use simple, friendly language. Avoid complex vocabulary.`,
},
});
```
## Locale Fallbacks
When a translation is missing, Idioma follows a fallback chain:
1. **Exact locale** (`es-MX`)
2. **Base locale** (`es` from `es-MX`)
3. **Default locale** (`en`)
4. **Source text** (always renders something)
This ensures your app always displays meaningful content, even with incomplete translations.
## Content-Addressable Keys
Idioma uses content-addressable keys—the message text determines the key:
1. **Murmurhash3** generates a 32-bit hash from the source message
2. **Base62 encoding** (0-9, A-Z, a-z) produces compact 8-character keys
3. Keys are **deterministic**: same input always produces the same key
```
"Hello {name}" → murmurhash3 → base62 → "a7Fk29xQ"
```
The `context` prop creates different keys for identical text:
```tsx
Submit // → "xK9mP2nL"
Submit // → "r4Yt8wQz" (different key)
```
### Choosing Your Key Strategy
Auto-generated keys are convenient but change when source text changes. Explicit IDs are stable across refactoring. Choose based on your needs:
| Strategy | Syntax | Best for |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------ |
| **Auto keys** | `Confirm Order` | Rapid iteration; AI retranslation is cheap |
| **Explicit IDs** | `` | Critical UI, legal text, formal review workflows |
| **Hybrid** | `Confirm Order` | Stable key + readable fallback |
Most teams use auto keys by default and explicit IDs for checkout flows, legal text, or anything requiring formal translation review.
## PO File Format
Idioma uses the standard [gettext PO format](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) with ICU MessageFormat in translations:
```po
# locales/es.po
# Simple message
msgid "Hello {name}"
msgstr "Hola {name}"
# With explicit context (from )
msgctxt "button"
msgid "Submit"
msgstr "Enviar"
# With AI-generated context (from idioma translate)
#. [AI Context]: Error message when payment fails at checkout
#: src/components/Checkout.tsx:87
msgid "Payment failed"
msgstr "El pago falló"
# Pluralization (ICU format)
msgid "{count, plural, one {# item} other {# items}}"
msgstr "{count, plural, one {# artículo} other {# artículos}}"
# Component interpolation (tag names from JSX)
msgid "Read our terms of service and privacy policy"
msgstr "Lee nuestros términos de servicio y política de privacidad"
```
PO files work with translation management systems like Phrase, Lokalise, and Crowdin.
## Bundle Optimization
Idioma automatically tree-shakes translations at the component level. Only the translations used by components on a given page are included in that page's bundle.
**How it works:**
1. The Babel plugin transforms each `` to reference specific translation keys
2. Each source file's translations are compiled to a separate chunk
3. The bundler includes only the chunks imported by components on each route
This means a 10,000-message app doesn't ship 10,000 messages to every page—each page gets only what it needs.
**What gets bundled:**
- ✅ Translations for components rendered on the page
- ❌ Translations for components on other routes
By default, all locale variants for those keys are included (enabling instant locale switching). For apps where initial bundle size is critical, Suspense mode loads only the active locale.
## Suspense Mode (React 19+)
For apps where initial load size is critical, Suspense mode takes optimization further: instead of bundling all locale variants, it loads only the active locale via dynamic imports.
```ts
// idioma.config.ts
export default defineConfig({
idiomaDir: './src/idioma',
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
useSuspense: true, // Enable lazy loading
});
```
Wrap your app with a Suspense boundary:
```tsx
import { IdiomaProvider } from '@/idioma';
}>
;
```
**Trade-offs:**
| | Default mode | Suspense mode |
| -------------------- | ---------------------- | ---------------------- |
| Translations bundled | Per-page (tree-shaken) | Per-page (tree-shaken) |
| Locales bundled | All locales | Active locale only |
| Locale switch | Instant | Suspends until loaded |
| React version | 18+ | 19+ (uses `use` hook) |
## API Reference
### Trans Component
| Prop | Type | Description |
| ------------ | ------------------------- | ---------------------------------------------- |
| `children` | `ReactNode` | Message content with interpolations |
| `id` | `string` | Explicit message key (alternative to children) |
| `context` | `string` | Translator context (affects key generation) |
| `ns` | `string` | Namespace for organizing translations |
| `values` | `Record` | Values for placeholder interpolation |
| `components` | `ReactNode[]` | Components for tag interpolation |
```tsx
// Children-based (auto-keyed)
Hello {name}
Submit
// ID-based (explicit key)
// Hybrid (stable key + fallback content)
Confirm Order
```
### useT Hook
```ts
const t = useT();
// Basic usage
t('Hello world'); // Simple message
t('Hello {name}', { name: 'Ben' }); // With values
t('Submit', undefined, { context: 'button' }); // With context
t('Save', undefined, { ns: 'common' }); // With namespace
// ID-based
t({ id: 'welcome' }); // Key only (translation must exist in PO)
t({ id: 'greeting', source: 'Hello {name}', values: { name: 'Ben' } }); // With source text + values
```
The `source` field provides default locale text for extraction (like children in `Source text`). Without `source`, the PO entry gets an empty `msgstr` and you must add translations manually.
### createT (Plain JS)
```ts
import { createT } from '@/idioma/plain';
const t = createT('es'); // Create for specific locale
// Same API as useT
t('Hello world');
t('Hello {name}', { name: 'Ben' });
t({ id: 'welcome' });
t({ id: 'greeting', source: 'Hello {name}', values: { name: 'Ben' } });
```
### ICU Helpers
```ts
import { plural, select, selectOrdinal } from '@idioma/core/icu';
// Pluralization
plural(count, { one: '# item', other: '# items' });
// Selection
select(gender, { male: 'He', female: 'She', other: 'They' });
// Ordinals
selectOrdinal(place, { one: '#st', two: '#nd', few: '#rd', other: '#th' });
```
### IdiomaProvider
```tsx
import { IdiomaProvider, useLocale } from '@/idioma';
;
// Get current locale
const locale = useLocale(); // 'en'
```
## How It Works
**Development:**
```
Hello {name}
↓ runs as-is
React context provides locale, renders translated text
```
**Production build:**
```
Hello {name}
↓ Babel transforms to
<__Trans __t={__$idioma.a7Fk29} __a={{name}} />
```
The Babel plugin extracts messages during build, generates content-addressed keys, and transforms components to use the compiled translation object.
## Comparison
| Feature | Idioma | Lingui | Paraglide | react-intl | i18next |
| ------------------------- | ------------- | ------------- | ------------ | ---------- | -------- |
| Runtime size (gzipped) | ~800B | ~3KB | ~2KB | ~14KB | ~22KB |
| Key management | Auto + Manual | Auto + Manual | Manual | Manual | Manual |
| Extraction | Built-in | Built-in | None | CLI | CLI |
| AI translation | Built-in | No | No | No | No |
| Compile-time | Yes | Yes | Yes | Optional | No |
| Component-level splitting | Yes | No | Yes | No | No |
| Lazy loading | Opt-in | Yes | Experimental | Yes | Yes |
| PO format | Yes | Yes | No | No | Plugin |
| Number/date formatting | Use Intl | Use Intl | Use Intl | Built-in | Built-in |
| SSR | Yes | Yes | Yes | Yes | Yes |
| RSC | Yes | Yes | Yes | No | Yes |
**Note on runtime size:** Idioma's ~800B runtime handles message rendering. ICU plural/select parsing adds ~2KB when you use those features. The total is still significantly smaller than alternatives because parsing happens at build time for static messages.
## Editor Support
TypeScript provides full type safety via generated types:
- **Message keys** — Autocomplete on `id` props and `t({ id: '...' })` calls
- **Required values** — Type errors when you forget interpolation values (`t('Hello {name}')` requires `{ name: string }`)
- **Locales** — Autocomplete and validation on locale strings
## Packages
- **@idioma/core** — Babel plugin, Vite plugin, Next.js plugin, Metro plugin, CLI, PO compiler
- **@idioma/react** — Runtime components (~800 bytes gzipped)
## License
MIT