pico-intl Svelte Themes

Pico Intl

Zero lock-in i18n for any JavaScript environment —, TypeScript-first, framework agnostic.

pico-intl

pico-intl

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.


What is pico-intl?

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.


Why pico-intl?

Size

@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

Performance — real numbers, measured on this machine

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

Zero lock-in — the only library with bidirectional migration

# 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.


Packages

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)

Quick Start

1. Install

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

2. Create locale files

// 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"
}

3. Create the instance

// 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.

4. Translate

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!"

Framework Adapters

React

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>
    </>
  );
}

Vue 3

<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>

Svelte 5

<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>

Next.js App Router

// 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>;
}

Astro

---
import { useAstroT } from '@pico-intl-dev/astro';
const t = useAstroT(Astro.currentLocale ?? 'en', { en, es });
---
<h1>{t('greeting', { name: 'Ana' })}</h1>

Angular

// app.config.ts
import { providePicoIntl } from '@pico-intl-dev/angular';
providers: [providePicoIntl(i18n)]

// my.component.ts — template
// <h1>{{ 'greeting' | translate:{ name: 'Ana' } }}</h1>

Vanilla / Node.js / Cloudflare Workers

import { createI18n } from '@pico-intl-dev/core';
// Works identically — no environment-specific setup

Core API

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

Locale detection

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'

Plural resolution (CLDR — all languages)

// 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

ICU select / selectordinal

{
  "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!"

RTL support

import { getTextDirection, isRTL, getDirectionAttr } from '@pico-intl-dev/core';

getTextDirection('ar')     // → 'rtl'
isRTL('he')                // → true
getDirectionAttr('ar')     // → { dir: 'rtl' }  (JSX/HTML spread)

CLI

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

IDE Support

TypeScript plugin — autocomplete, hover, go-to-definition

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

i18n-ally integration

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 Compatibility

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.


Zero Telemetry

pico-intl collects nothing. No analytics, no usage tracking, no accounts, no phone-home calls. Ever. The source is open — verify it yourself.


Project Structure

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

Development

# 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

Contributing

Read CONTRIBUTING.md for setup, conventions, and the zero-telemetry pledge.

  1. Fork → feature branch → git checkout -b feat/my-feature
  2. Make changes + add tests to test/e2e.mts
  3. npm run build && npx tsx test/e2e.mts — must be green
  4. Open a PR

License

MIT © 2026 pico-intl contributors

Your translations. Your code. Your freedom.

Top categories

Loading Svelte Themes