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

@pie-qti/i18n

v0.1.0

Published

Internationalization (i18n) package for PIE-QTI player ecosystem

Readme

@pie-qti/i18n

Lightweight, type-safe internationalization (i18n) package for the PIE-QTI player ecosystem.

Features

  • Type-Safe Translations - TypeScript autocomplete for all message keys
  • Locale Persistence - Locale persists across page reloads via localStorage
  • Framework Agnostic - Core provider works with any JavaScript framework
  • Svelte Integration - Simple $derived pattern for Svelte 5 components
  • Web Component Compatible - Works within Shadow DOM boundaries
  • Small Bundle Size - <10 KB gzipped for core + default locale
  • On-Demand Loading - Load additional locales asynchronously
  • Built-in Formatting - Number and date formatting via Intl APIs
  • Custom Translations - Override or extend framework translations
  • 6 Languages Supported - en-US (complete), es-ES, fr-FR, nl-NL, ro-RO, th-TH

Installation

bun add @pie-qti/i18n

Quick Start

1. Create i18n Provider

import { DefaultI18nProvider } from '@pie-qti/i18n';

// Create provider with default locale
const i18n = new DefaultI18nProvider('en-US');

// Optional: Load additional locales
await i18n.loadLocale('es-ES');
await i18n.loadLocale('fr-FR');

2. Use in Components (Svelte 5)

<script lang="ts">
import type { I18nProvider } from '@pie-qti/i18n';

interface Props {
  i18n?: I18nProvider;  // Always optional
}

let { i18n }: Props = $props();

// Simple pattern - locale changes trigger page refresh
const translations = $derived({
  submit: i18n?.t('common.submit') ?? 'Submit',
  cancel: i18n?.t('common.cancel') ?? 'Cancel'
});
</script>

<button>{translations.submit}</button>
<button>{translations.cancel}</button>

Note: Translations are evaluated once when the component loads. If you need to update translations, the page will reload when the locale changes.

3. Switch Locales

// Switch to Spanish (triggers page reload)
await i18n.setLocale('es-ES');

// Locale is stored in localStorage and persists across reloads
// On next page load, i18n will automatically use 'es-ES'

Important: Calling setLocale() stores the locale in localStorage and triggers a page reload. This simplifies the codebase by eliminating complex reactivity patterns.

API Reference

Core Provider

I18nProvider Interface

The framework-agnostic interface for i18n providers:

interface I18nProvider {
  getLocale(): string;
  setLocale(locale: string): Promise<void>;
  loadLocale(locale: string): Promise<void>;
  t(key: string, values?: Record<string, any>): string;
  formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
  formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string;
}

DefaultI18nProvider

Basic implementation without framework-specific features:

import { DefaultI18nProvider } from '@pie-qti/i18n';

// Create with default locale
const i18n = new DefaultI18nProvider('en-US');

// With custom messages
const customMessages = {
  'es-ES': {
    common: { submit: 'Enviar' }
  }
};
const i18n = new DefaultI18nProvider('en-US', customMessages);

// Load additional locales
await i18n.loadLocale('es-ES');

// Switch locales
await i18n.setLocale('es-ES');

// Translate
const text = i18n.t('common.submit');
const interpolated = i18n.t('assessment.question', { current: 1, total: 10 });

SvelteI18nProvider

Svelte-specific wrapper that extends I18nProvider:

import { createDefaultSvelteI18nProvider } from '@pie-qti/i18n';

// Create Svelte provider
const i18n = await createDefaultSvelteI18nProvider('en-US');

// Same API as I18nProvider
const text = i18n.t('common.submit');
await i18n.setLocale('es-ES'); // Triggers page reload

// Deprecated reactive stores (kept for backwards compatibility)
// Note: These are no longer needed since locale changes trigger page reload
const { locale, t$, plural$, formatNumber$, formatDate$ } = i18n;

Migration Note: The reactive stores (locale, t$, plural$, etc.) are deprecated. Use the direct methods (t(), plural(), etc.) with $derived instead. See the Quick Start section for the recommended pattern.

Supported Locales

The framework currently includes 6 locales:

| Locale Code | Language | Status | |-------------|---------------------------|--------------------------| | en-US | English (United States) | ✅ Complete (300+ keys) | | es-ES | Spanish (Spain) | 🚧 ~80% complete | | fr-FR | French (France) | 🚧 ~70% complete | | nl-NL | Dutch (Netherlands) | 🚧 ~30% complete | | ro-RO | Romanian (Romania) | 🚧 ~30% complete | | th-TH | Thai (Thailand) | 🚧 ~30% complete |

Note: Additional locales can be provided via custom translations (see below).

Message Keys

All translation keys are type-safe. Your IDE will provide autocomplete for available keys:

// ✅ Valid keys (autocomplete provided)
$t('common.submit')
$t('interactions.upload.label')
$t('assessment.question', { current: 1, total: 10 })

// ❌ TypeScript error - invalid key
$t('invalid.key')

Common Message Namespaces

  • common.* - Shared UI text (buttons, labels)
  • units.* - Unit formatting (bytes, time)
  • validation.* - Form validation messages
  • interactions.* - QTI interaction-specific text
  • assessment.* - Assessment player UI
  • accessibility.* - ARIA labels and screen reader text

See en-US.ts for the complete list of available keys.

Custom Translations

You can provide your own translations or override framework translations:

import { DefaultI18nProvider } from '@pie-qti/i18n';

const customMessages = {
  // Add a new locale
  'de-DE': {
    common: {
      submit: 'Einreichen',
      next: 'Weiter',
      previous: 'Zurück',
    },
    interactions: {
      upload: {
        label: 'Datei hochladen',
      },
    }
  },
  // Override specific framework translations
  'en-US': {
    common: {
      submit: 'Send Answer', // Brand-specific terminology
    }
  }
};

const i18n = new DefaultI18nProvider('en-US', customMessages);

Custom translations have higher priority than framework translations, so you can selectively override specific keys while keeping the rest.

Component Integration

Recommended Pattern (Svelte 5)

The recommended approach is simple: use $derived to create memoized translations.

<script lang="ts">
import type { I18nProvider } from '@pie-qti/i18n';

interface Props {
  i18n?: I18nProvider;
}

let { i18n }: Props = $props();

// Memoized translations - evaluated once when component loads
const translations = $derived({
  submit: i18n?.t('common.submit') ?? 'Submit',
  cancel: i18n?.t('common.cancel') ?? 'Cancel'
});
</script>

<button class="btn btn-primary">{translations.submit}</button>
<button class="btn btn-outline">{translations.cancel}</button>

Inline Pattern (For Simple Cases)

For one-off translations, you can call t() directly:

<button>{i18n?.t('common.submit') ?? 'Submit'}</button>

Helper Pattern (For Cleaner Code)

Create a translation helper for repeated use:

<script lang="ts">
const t = $derived((key: string, fallback: string) => i18n?.t(key) ?? fallback);
</script>

<button>{t('common.submit', 'Submit')}</button>
<button>{t('common.cancel', 'Cancel')}</button>

Web Components

Web components receive i18n as a property:

interface Props {
  i18n?: I18nProvider;
}

let { i18n = $bindable() }: Props = $props();

const t = $derived((key: string, fallback: string) => i18n?.t(key) ?? fallback);

Note: No manual subscriptions or reactivity tracking needed. Locale changes trigger a page reload, so components always render with the correct locale.

Bundle Size

| Component | Size (gzipped) | |-------------------------------|----------------| | Core i18n logic | ~2 KB | | English locale (en-US) | ~8 KB | | Additional locale (on-demand) | ~8 KB each | | Total (default) | ~10 KB |

Documentation

For detailed guides, see the docs/ directory:

Development

Adding New Messages

  1. Add to English locale:

    // src/locales/en-US.ts
    export default {
      myFeature: {
        newMessage: 'My new message',
      },
    } as const;
  2. Add to other locales (maintain same structure)

  3. Run translation coverage checker:

    bun run check-translations
  4. TypeScript automatically provides type safety for the new key

Translation Coverage Checker

The package includes an automated translation coverage checker that ensures all locales maintain 100% coverage:

# Check all locales
bun run check-translations

# Check specific locale
bun run check-translations:locale=nl-NL

The checker:

  • Automatically discovers all keys from en-US (no hardcoded lists)
  • Automatically detects all locale files (no configuration)
  • Reports missing keys in other locales
  • Detects obsolete keys that should be removed
  • Runs in CI/CD to block PRs with incomplete translations

Integrated into build:

bun run build  # Automatically checks translations first

See Translation Coverage Documentation for details.

Hardcoded String Scanner

The package includes a scanner that detects hardcoded English strings in component files that should use i18n:

# Scan default components package
bun run scan-hardcoded

# Scan a custom path
bun run scan-hardcoded --path=../custom-components

The scanner:

  • Detects hardcoded strings that match translation values in en-US
  • Suggests the correct i18n key to use for each match
  • Scans .svelte and .ts files recursively
  • Provides exact replacement code for each hardcoded string

Example output:

📄 plugins/graphic-order/GraphicOrderInteraction.svelte (1 match)
────────────────────────────────────────────────────────────────
  Line 138: <h3>Order (drag to reorder)</h3>
           Use: i18n?.t('interactions.graphicOrder.orderHeading') ?? 'Order (drag to reorder)'

This tool helps maintain consistency by ensuring all user-facing strings use the i18n system.

Running Tests

bun test

Building

# Build with translation check
bun run build

# Build without translation check (emergency use only)
bun run build:skip-check

License

ISC © Renaissance Learning