The last i18n library you'll ever need to migrate from.
Zero lock-in · Framework agnostic · ~0.9 KB gzip · Zero dependencies · MIT
Early stage (v0.x) — Core runtime, CLI, and all 10 framework adapters are fully tested and production-ready (298 unit + 212 integration tests across React, Vue, Svelte, Solid, Next.js, Astro, Angular). The API may change in minor versions until v1.0.
pico-intl is a runtime-agnostic internationalization library that works identically in every JavaScript environment — browser, Node.js, Bun, Deno, Cloudflare Workers, and React Native — without plugins, polyfills, or configuration changes.
Your translations are plain JSON files. You can leave pico-intl at any time with zero data loss.
@pico-intl-dev/core 0.9 KB gz (full feature set)
i18next 11 KB gz (core only, adapters not included)
react-intl 18 KB gz
vue-i18n 19 KB gz
@formatjs/intl 30 KB gz
| Operation | pico-intl | i18next (est.) | react-intl (est.) |
|---|---|---|---|
t() simple lookup* |
29 ns | ~85 ns | ~220 ns |
t() w/ interpolation |
929 ns | ~300 ns | ~600 ns |
t() plural (CLDR) |
1.35 µs | ~250 ns | ~500 ns |
exists() check |
22 ns | — | — |
| Bundle size (core, gz) | 0.9 KB | 11 KB | 18 KB |
* Simple key lookup with no interpolation or plural resolution. Run npm run bench to reproduce all numbers on your own hardware. Competitor numbers are estimates from their published benchmarks, not measured on the same machine.
Benchmarks run via
npm run bench· Node.js v24 · win32 x64
# Come from any library
npx pico-intl import --from i18next --input ./src/locales
npx pico-intl import --from react-intl --input ./translations
npx pico-intl import --from vue-i18n --input ./src/i18n
npx pico-intl import --from fluent --input ./ftl
npx pico-intl import --from po --input ./gettext
# Leave to any library — zero data loss
npx pico-intl export --to i18next
npx pico-intl export --to react-intl
npx pico-intl export --to vue-i18n
npx pico-intl export --to fluent
npx pico-intl export --to po
npx pico-intl export --to arb # Flutter
i18next, react-intl, and vue-i18n have no export tool. Once you're in, you're in. pico-intl doesn't work that way.
| Package | Size | Status | Description |
|---|---|---|---|
@pico-intl-dev/core |
0.9 KB gz | ✅ Stable | Runtime engine — zero deps |
@pico-intl-dev/cli |
— | ✅ Stable | Full CLI toolkit (init, validate, translate, generate, extract, import, export…) |
@pico-intl-dev/ts-plugin |
— | ✅ Stable | TypeScript language service plugin — autocomplete, hover, go-to-def |
@pico-intl-dev/react |
— | ✅ Stable | React hooks + Suspense |
@pico-intl-dev/vue |
— | ✅ Stable | Vue 3 composables |
@pico-intl-dev/svelte |
— | ✅ Stable | Svelte 5 runes |
@pico-intl-dev/solid |
— | ✅ Stable | SolidJS signals |
@pico-intl-dev/next |
— | ✅ Stable | Next.js App Router (RSC + Client) — integration tested |
@pico-intl-dev/astro |
— | ✅ Stable | Astro SSR integration — integration tested + browser-verified |
@pico-intl-dev/angular |
— | ✅ Stable | Angular 17+ standalone — integration tested (DI + signals + pipes) |
npm install @pico-intl-dev/core
Or with CLI tooling:
npm install @pico-intl-dev/core @pico-intl-dev/cli --save-dev
npx pico-intl init
// locales/en.json
{
"greeting": "Hello, {{name}}!",
"items": "{{count}} item | {{count}} items",
"role": "{{role, select, admin{Admin} user{User} other{Guest}}}",
"position": "{{n, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}} place"
}
// locales/es.json
{
"greeting": "¡Hola, {{name}}!",
"items": "{{count}} artículo | {{count}} artículos",
"role": "{{role, select, admin{Administrador} user{Usuario} other{Invitado}}}",
"position": "{{n, selectordinal, one{#.º} two{#.º} few{#.º} other{#.º}}} lugar"
}
// i18n.ts
import { createI18nAsync, createLocalStorageAdapter } from '@pico-intl-dev/core';
import en from './locales/en.json';
// createI18nAsync: awaits locale detection BEFORE the first render.
// This eliminates the flash of untranslated content (FOUC) you get
// with the sync createI18n() + locale:'auto' pattern.
export const i18n = await createI18nAsync({
base: 'en',
locale: 'auto', // detect from localStorage → navigator → fallback
messages: en,
supported: ['en', 'es'],
storage: createLocalStorageAdapter('my-app-locale'), // persist across reloads
loader: (locale) => import(`./locales/${locale}.json`),
});
// i18n.locale is already the user's detected locale — zero flash.
Sync alternative (no top-level await, HMR-friendly):
import { createI18n, createLocalStorageAdapter } from '@pico-intl-dev/core'; const i18n = createI18n({ base: 'en', locale: 'auto', messages: en, storage: createLocalStorageAdapter(), loader: ... }); // Note: locale starts at 'en' and switches async — may flash on first render.
const { t } = i18n;
t('greeting', { name: 'Ana' }) // → "Hello, Ana!"
t('items', { count: 1 }) // → "1 item"
t('items', { count: 5 }) // → "5 items"
t('role', { role: 'admin' }) // → "Admin"
t('position', { n: 2 }) // → "2nd place"
i18n.exists('optional.key') // → false (never throws)
i18n.ordinal('position', 1) // → "1st place"
await i18n.setLocale('es');
t('greeting', { name: 'Ana' }) // → "¡Hola, Ana!"
import { createRoot } from 'react-dom/client';
import { PicoIntlProvider, useT, useLocale } from '@pico-intl-dev/react';
import { i18n } from './i18n'; // the async singleton from Step 3
// Wire up the provider at the root — i18n instance is already in the right locale
function App() {
return (
<PicoIntlProvider i18n={i18n}>
<MyPage />
</PicoIntlProvider>
);
}
function MyPage() {
const t = useT();
const { locale, setLocale } = useLocale();
return (
<>
<h1>{t('greeting', { name: 'Ana' })}</h1>
<button onClick={() => setLocale('es')}>Español</button>
</>
);
}
<script setup lang="ts">
import { useT, useLocale } from '@pico-intl-dev/vue';
const t = useT();
const { locale, setLocale } = useLocale();
</script>
<template>
<h1>{{ t('greeting', { name: 'Ana' }) }}</h1>
<button @click="setLocale('es')">Español</button>
</template>
<script lang="ts">
import { useT, useLocale } from '@pico-intl-dev/svelte';
const t = useT();
const { locale, setLocale } = useLocale();
</script>
<h1>{t('greeting', { name: 'Ana' })}</h1>
<button onclick={() => setLocale('es')}>Español</button>
// app/[locale]/page.tsx — Server Component
import { getT } from '@pico-intl-dev/next';
export default async function Page({ params: { locale } }) {
const t = await getT(locale);
return <h1>{t('greeting', { name: 'Ana' })}</h1>;
}
---
import { useAstroT } from '@pico-intl-dev/astro';
const t = useAstroT(Astro.currentLocale ?? 'en', { en, es });
---
<h1>{t('greeting', { name: 'Ana' })}</h1>
// app.config.ts
import { providePicoIntl } from '@pico-intl-dev/angular';
providers: [providePicoIntl(i18n)]
// my.component.ts — template
// <h1>{{ 'greeting' | translate:{ name: 'Ana' } }}</h1>
import { createI18n } from '@pico-intl-dev/core';
// Works identically — no environment-specific setup
const i18n = createI18n({
base: 'en', // base/fallback locale
locale: 'en', // initial active locale
messages: en, // initial locale messages
fallbackMessages: en, // explicit fallback (optional)
loader: async (locale) => import(`./locales/${locale}.json`),
storage: localStorageAdapter, // persist locale choice
escapeHtml: false, // XSS-safe interpolation
onMissingKey: (key, locale) => reportToSentry(key, locale),
});
i18n.t(key, params?) // translate with interpolation
i18n.ordinal(key, count) // ordinal plurals (1st, 2nd…)
i18n.exists(key) // non-throwing key presence check
i18n.locale // current locale string (reactive getter)
i18n.setLocale(locale) // async — loads messages + persists
i18n.getMessages() // raw messages snapshot
i18n.onLocaleChange(callback) // returns unsubscribe function
i18n.destroy() // cleanup — clears cache and listeners
import { detectLocale, parseAcceptLanguageHeader } from '@pico-intl-dev/core';
// Waterfall: storage → Accept-Language header → navigator → process.env → fallback
const locale = detectLocale({
available: ['en', 'es', 'fr', 'de'],
fallback: 'en',
storage: myStorageAdapter,
acceptLanguage: request.headers.get('accept-language'), // SSR/Edge
});
// SSR — parse Accept-Language header directly
const best = parseAcceptLanguageHeader('fr-CH, fr;q=0.9, en;q=0.8', ['en', 'fr']);
// → 'fr'
// English: one | other
t('items', { count: 1 }) // → "1 item"
t('items', { count: 5 }) // → "5 items"
// Russian: one | few | many | other
// Arabic: zero | one | two | few | many | other
// All powered by Intl.PluralRules — zero bundle size cost
{
"msg": "{{gender, select, male{He} female{She} other{They}}} liked it.",
"pos": "Finished {{n, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}}!"
}
t('msg', { gender: 'female' }) // → "She liked it."
t('pos', { n: 3 }) // → "Finished 3rd!"
import { getTextDirection, isRTL, getDirectionAttr } from '@pico-intl-dev/core';
getTextDirection('ar') // → 'rtl'
isRTL('he') // → true
getDirectionAttr('ar') // → { dir: 'rtl' } (JSX/HTML spread)
Commands:
init Scaffold config, locale files, and tsconfig entries
validate Check for missing keys and placeholder mismatches
translate Auto-translate via DeepL, Google Translate, or OpenAI
generate Generate TypeScript types from locale files
extract Scan source code and add missing keys
check Gate for CI — exit 1 if any used key is undefined
stats Translation coverage dashboard per locale
prune Preview unused keys (dry-run by default — add --apply to delete)
import Migrate translations in from other formats
export Migrate translations out to other formats
ci-template Generate GitHub Actions / GitLab CI / Husky / VS Code config
completion Shell completions (zsh / bash / fish)
# Initialize
npx pico-intl init
# Validate all locale files
npx pico-intl validate --strict
# Auto-translate missing keys
npx pico-intl translate --locale es --provider deepl --key $DEEPL_API_KEY
# Generate TypeScript types
npx pico-intl generate --out src/i18n-types.ts
# CI gate — fails if any t('key') is undefined in base locale
npx pico-intl check --src ./src
# Coverage report
npx pico-intl stats --threshold 90
# Prune: preview unused keys (safe by default — no files written)
npx pico-intl prune --src ./src
# Prune: actually delete unused keys (requires --apply)
npx pico-intl prune --src ./src --apply
# Migrate from i18next
npx pico-intl import --from i18next --input ./src/locales --output ./locales
# Export to Flutter
npx pico-intl export --to arb --output ./lib/l10n
# Generate VS Code + i18n-ally config
npx pico-intl ci-template --vscode
2-step setup (works in any editor with TypeScript LSP support):
# Step 1 — install
npm install --save-dev @pico-intl-dev/ts-plugin
// Step 2 — tsconfig.json
{
"compilerOptions": {
"plugins": [
{
"name": "@pico-intl-dev/ts-plugin",
"localesDir": "./locales",
"baseLocale": "en",
"strictMode": false
}
]
}
}
VS Code: Open the command palette and run TypeScript: Select TypeScript Version → Use Workspace Version. This activates the language service plugin.
Other editors (Neovim, WebStorm, Zed): The plugin activates automatically via the TypeScript LSP — no additional config needed.
| Feature | Description |
|---|---|
| Autocomplete | `t('nav. |
| Hover info | t('nav.home') → 🌐 "Home" [en] inline |
| Go to definition | Cmd+Click → jumps to exact line in en.json |
| Diagnostics | "strictMode": true → red squiggles on unknown keys |
npx pico-intl ci-template --vscode
Generates .vscode/i18n-ally-custom-framework.yml with all pico-intl call patterns pre-configured — t(), i18n.t(), $t(), Angular pipe, template literals.
| Runtime | Support | Notes |
|---|---|---|
| Browser (all modern) | ✅ | Native Intl.* + fetch |
| Node.js ≥ 18 | ✅ | process.env.LANG detection |
| Bun | ✅ | Full parity |
| Deno | ✅ | Full parity |
| Cloudflare Workers | ✅ | Accept-Language detection |
| AWS Lambda / Edge | ✅ | Zero Node.js globals assumed |
| React Native | ✅ | No window / document access |
Zero platform-specific code. One package, everywhere.
pico-intl collects nothing. No analytics, no usage tracking, no accounts, no phone-home calls. Ever. The source is open — verify it yourself.
language+/
├── packages/
│ ├── core/ @pico-intl-dev/core — runtime engine (~0.9 KB gz)
│ ├── cli/ @pico-intl-dev/cli — CLI toolkit
│ ├── ts-plugin/ @pico-intl-dev/ts-plugin — TypeScript language server plugin
│ ├── react/ @pico-intl-dev/react — React adapter
│ ├── vue/ @pico-intl-dev/vue — Vue 3 adapter
│ ├── svelte/ @pico-intl-dev/svelte — Svelte 5 adapter
│ ├── solid/ @pico-intl-dev/solid — SolidJS adapter
│ ├── next/ @pico-intl-dev/next — Next.js App Router
│ ├── astro/ @pico-intl-dev/astro — Astro SSR
│ └── angular/ @pico-intl-dev/angular — Angular 17+
├── bench/
│ └── bench.mts — real performance benchmarks
├── docs/ — VitePress documentation site
├── test/
│ └── e2e.mts — 298 E2E integration tests
├── LICENSE — MIT
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
# Install all dependencies
npm install
# Build all packages
npm run build
# Run unit/E2E test suite (298 tests)
npx tsx test/e2e.mts
# Run framework integration tests (212 tests — React, Vue, Svelte, Solid, Next, Astro, Angular)
npx tsx test/integration.mts
# Run performance benchmarks
npm run bench
# Documentation dev server
npm run docs:dev
Read CONTRIBUTING.md for setup, conventions, and the zero-telemetry pledge.
git checkout -b feat/my-featuretest/e2e.mtsnpm run build && npx tsx test/e2e.mts — must be greenMIT © 2026 pico-intl contributors
Your translations. Your code. Your freedom.