npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@astroscope/i18n

v0.4.0

Published

i18n for Astro + React islands — MessageFormat 2, automatic tree-shaking, parallel loading, any translation source

Readme

@astroscope/i18n

Note: This package is in active development. APIs may change between versions.

i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source.

Why this library?

SSR-first — The only i18n solution built specifically for SSR + islands architecture. Works seamlessly with Astro's partial hydration model where most i18n libraries fail.

Automatic tree-shaking — Only translations actually used by each component are delivered to the browser. No manual chunk splitting, no configuration. It just works.

Parallel loading — Translations load alongside component hydration via custom client:*-x directives. No waiting for translations before rendering.

Unified API — Same t() function works identically in Astro templates and React islands.

Any translation source — Fetch translations from any provider: JSON files, database, headless CMS, TMS, or custom API. All of them will be optimized and chunked automatically.

Production optimized — Fallback strings are stripped from production bundles via Babel, reducing bundle size while keeping fallbacks available via the manifest.

Features

  • Per-chunk translation loading — each island gets only its translations
  • Unicode MessageFormat 2 (MF2) support via messageformat v4
  • Built-in formatters:number, :integer, :percent, :currency, :date, :time, :datetime, :unit
  • Babel-based extraction — robust AST parsing, source maps, production stripping
  • Manifest fallbacks — missing translations automatically use extracted fallbacks
  • Full TypeScript support
  • Tiny client runtime — ~8KB gzipped for translations

Installation

npm install @astroscope/i18n @astroscope/boot @astroscope/excludes

Usage

1. Add the integration

// astro.config.ts
import { defineConfig } from 'astro/config';
import boot from '@astroscope/boot';
import i18n from '@astroscope/i18n';

export default defineConfig({
  integrations: [
    boot(),
    i18n(),
  ],
});

Consistency checking

The integration checks for translation key consistency — when the same key is used in multiple files with different fallbacks, variables, or descriptions.

i18n({
  consistency: 'warn',  // 'warn' (default) | 'error' | 'off'
})
  • 'warn' — Log a warning but continue (default)
  • 'error' — Fail the build
  • 'off' — Disable consistency checking

2. Configure i18n in your boot file

VERY IMPORTANT: i18n.configure must be awaited during boot before handling any requests!

// src/boot.ts
import { i18n, type RawTranslations } from '@astroscope/i18n';

async function fetchTranslations(locale: string): Promise<RawTranslations> {
  // fetch from your CMS, API, or local files
  const response = await fetch(`https://api.example.com/translations/${locale}`);
  return response.json();
}

export async function onStartup() {
  await i18n.configure({
    locales: ['en', 'de'],
    defaultLocale: 'en',  // optional, defaults to first locale
  });

  // load translations for all locales
  const [en, de] = await Promise.all([
    fetchTranslations('en'),
    fetchTranslations('de'),
  ]);

  i18n.setTranslations('en', en);
  i18n.setTranslations('de', de);
}

3. Add the middleware

// src/middleware.ts
import { sequence } from 'astro:middleware';
import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';

export const onRequest = sequence(
  createI18nChunkMiddleware(),  // serves /_i18n/ translation chunks
  createI18nMiddleware({
    locale: (ctx) =>
      ctx.cookies.get('locale')?.value ??
      i18n.getConfig().defaultLocale,
  }),
);

By default, RECOMMENDED_EXCLUDES (static assets like /_astro/) are excluded from locale context setup. To customize:

import { sequence } from 'astro:middleware';
import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';
import { RECOMMENDED_EXCLUDES } from '@astroscope/excludes';

export const onRequest = sequence(
  createI18nChunkMiddleware(),
  createI18nMiddleware({
    locale: (ctx) =>
      ctx.cookies.get('locale')?.value ??
      i18n.getConfig().defaultLocale,
    exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
  }),
);

4. Add <I18nScript /> to your layout

Inject translations into the page for hydrated components:

---
import { I18nScript } from '@astroscope/i18n/astro';
---
<html>
  <head>
    <I18nScript />
  </head>
  <body>
    <slot />
  </body>
</html>

5. Use t() in your components

---
// In .astro files
import { t } from '@astroscope/i18n/translate';
---
<h1>{t('checkout.title', 'Order Summary')}</h1>
// In React components
import { t } from '@astroscope/i18n/translate';

export function CheckoutSummary() {
  return (
    <div>
      <h1>{t('checkout.title', 'Order Summary')}</h1>
      <p>{t('checkout.tax', 'Includes {$tax} VAT', { tax: '19%' })}</p>
    </div>
  );
}

Note: Variables use {$name} syntax (with $ prefix) per MessageFormat 2 specification.

6. Use i18n-aware client directives

The problem: With standard client:* directives, the translation chunk loads after the component module. This delays hydration while translations are fetched sequentially.

The solution: Use client:*-x directives to load translations in parallel with the component code:

---
import Cart from '../components/Cart';
---
<!-- translations load alongside component code -->
<Cart client:load-x />
<Cart client:visible-x />
<Cart client:idle-x />

API

t(key, fallback, values?)

Translate a key with optional interpolation values. The fallback is used when a translation is missing and also serves as an example for translators. Uses MessageFormat 2 syntax.

// simple text
t('checkout.title', 'Order Summary')

// with variables (note the $ prefix)
t('checkout.tax', 'Includes {$tax} VAT', { tax: '19%' })

// with pluralization (MF2 syntax)
t('cart.items', `.input {$count :number}
.match $count
one {{{$count} item}}
* {{{$count} items}}`, { count: 5 })

// with number formatting
t('stats.value', '{$value :number minimumFractionDigits=2}', { value: 1234.5 })

// with percentage
t('stats.ratio', '{$value :percent}', { value: 0.856 })

// with date/time formatting
t('event.date', '{$date :date style=long}', { date: new Date() })
t('event.time', '{$time :time style=short}', { time: new Date() })

// with currency (translator controls currency)
t('product.price', '{$price :currency currency=EUR}', { price: 99.99 })

// with currency (code controls currency via wrapped value)
t('product.price', '{$price :currency}', {
  price: { valueOf: () => 99.99, options: { currency: 'EUR' } }
})

// with units
t('distance', '{$value :unit unit=kilometer}', { value: 42 })

// with metadata object (for extraction tooling)
t('cart.total', {
  example: 'Total: {$amount}',
  description: 'Cart total price',
  variables: {
    amount: { example: '$0.00', description: 'Formatted price' }
  }
}, { amount: '$49.99' })

rich(key, fallback, components, values?)

Translate with embedded components using MF2 markup syntax. Returns an array of strings and JSX elements that can be rendered directly.

import { rich } from '@astroscope/i18n/translate';

// basic link
rich('tos', 'Read our {#link}Terms of Service{/link}', {
  link: (children) => <a href="/tos">{children}</a>
})
// Returns: ['Read our ', <a href="/tos">Terms of Service</a>]

// multiple tags
rich('legal', 'Read our {#tos}Terms{/tos} and {#privacy}Privacy Policy{/privacy}', {
  tos: (children) => <a href="/tos">{children}</a>,
  privacy: (children) => <a href="/privacy">{children}</a>,
})

// with variables
rich('greeting', 'Hello {$name}, check your {#inbox}messages{/inbox}', {
  inbox: (children) => <a href="/inbox">{children}</a>,
}, { name: 'Alice' })

// nested tags
rich('highlight', 'This is {#bold}very {#em}important{/em}{/bold}', {
  bold: (children) => <strong>{children}</strong>,
  em: (children) => <em>{children}</em>,
})

// standalone (self-closing) tags
rich('install', 'Click {#icon/} to install', {
  icon: () => <DownloadIcon />,
})

The same code works in both Astro templates and React islands — rich() is JSX-runtime agnostic.

i18n singleton

import { i18n } from '@astroscope/i18n';

// configure (call once at startup)
await i18n.configure({
  locales: ['en', 'de'],
  defaultLocale: 'en',
});

// set translations for a locale
i18n.setTranslations('en', { 'key': 'value' });

// get raw translations (includes manifest fallbacks)
i18n.getTranslations('en');

// get compiled translations (ICU MessageFormat functions)
i18n.getCompiledTranslations('en');

// get extraction manifest
// has all extracted keys with their metadata
// you can use this to generate translation files or upload to a TMS / CMS
i18n.getManifest();

// clear cached translations
i18n.clear();        // all locales
i18n.clear('en');    // specific locale

Lazy loading with React.lazy()

Translations load automatically for lazy-loaded components.

import { Suspense, lazy } from 'react';

const StatsModal = lazy(() => import('./StatsModal'));

export function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <StatsModal />
    </Suspense>
  );
}

How it works

  1. Build time — Babel plugin extracts all t() calls, maps them to chunks, strips fallbacks from production bundles
  2. Manifest — Extracted keys with fallbacks are written to i18n-manifest.json
  3. SSR — Middleware provides translations to t(), merging manifest fallbacks for missing keys
  4. Client — Custom directives load only the translations needed by each chunk

The same import { t } from '@astroscope/i18n/translate' works everywhere — bundler picks the correct implementation via conditional exports (browser vs default).

Client bundle

Translation chunks are served as raw MessageFormat 2 strings and compiled on the browser on first use. This keeps chunk sizes minimal — the messageformat runtime is ~8KB gzipped. Compiled messages are cached for subsequent renders.

Future: Once browsers ship native Intl.MessageFormat, this 8KB runtime will be replaced by the built-in API with zero bundle cost.

MessageFormat 2 Syntax

This library uses Unicode MessageFormat 2 (MF2), the modern standard for internationalization.

Basic syntax

Simple text
Hello {$name}

Pluralization

.input {$count :number}
.match $count
one {{{$count} item}}
* {{{$count} items}}

Selection (gender, etc.)

.input {$gender :string}
.match $gender
male {{He liked it}}
female {{She liked it}}
* {{They liked it}}

Built-in formatters

| Formatter | Description | Example | |-----------|-------------|---------| | :number | Locale-aware number | {$n :number} → "1,234.56" | | :integer | Integer (no decimals) | {$n :integer} → "1,235" | | :percent | Percentage | {$n :percent} → "85.6%" | | :currency | Currency | {$n :currency currency=EUR} → "€99.99" | | :date | Date | {$d :date style=long} → "January 26, 2026" | | :time | Time | {$d :time style=short} → "3:45 PM" | | :datetime | Date + time | {$d :datetime dateStyle=medium timeStyle=short} | | :unit | Units | {$n :unit unit=kilometer} → "42 km" |

Currency and unit options

For :currency and :unit, the required option (currency or unit) can be:

  1. Hardcoded in translation (translator controls):

    {$price :currency currency=EUR}

    Code passes plain number: { price: 99.99 }

  2. Provided by code (for dynamic currency/unit):

    {$price :currency}

    Code passes wrapped value: { price: { valueOf: () => 99.99, options: { currency: 'EUR' } } }

Note: If both translation and code specify the option, translation wins.

Configuration

i18n.configure()

| Option | Type | Default | Description | |--------|------|---------|-------------| | locales | string[] | required | Supported locales | | defaultLocale | string | first locale | Default/fallback locale | | fallback | FallbackBehavior | 'fallback' | Behavior when translation missing |

FallbackBehavior

  • 'fallback' — Use the fallback string from manifest (default)
  • 'key' — Return the translation key
  • 'throw' — Throw an error
  • (key, meta) => string — Custom function

ESLint Plugin

Use @astroscope/eslint-plugin-i18n to enforce correct t() usage, catch build-time extraction issues, and ensure i18n-aware hydration directives.

npm install -D @astroscope/eslint-plugin-i18n
// eslint.config.js
import i18n from '@astroscope/eslint-plugin-i18n';

export default [
  i18n.configs.recommended,
];

License

MIT