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

@websnacksjs/i18n

v0.1.0

Published

A lightweight, zero-dependency, isomorphic internationalization (i18n) library for modern build pipelines.

Readme

@websnacksjs/i18n

A lightweight, zero-dependency, isomorphic internationalization (i18n) library for modern build pipelines.

Designed as a simpler alternative to i18next with a fail-fast and convention-over-configuration philosophy.

✨ Features

  • 🛡️ Type-safe – Catch typos and missing translation keys at build time with strongly-typed keys.
  • 🚫 Fails-fast – Ensure your team catches translation issues before they impact end users.
  • 📦 Zero runtime dependencies – Nothing extra to ship or transitive dependencies to monitor for security vulnerabilities.
  • 🪶 Simple & standards-compliant – Intuitive & highly interoperable with browser & node Intl APIs.

📦 Installation

npm install @websnacksjs/i18n
# or
yarn add @websnacksjs/i18n
# or
pnpm add @websnacksjs/i18n

🚀 Quick Start

./messages/en/common.json

{
  "hello": "Hello, {{name}}!"
}

./messages/fr-Arab/common.json

{
  "hello": "بونجور \u2066{{name}}\u2069!"
}

import I18n from "@websnacksjs/i18n";

const i18n = new I18n<{
 common: typeof import("./messages/en/common.json")
}>({
  supportedLocales: ["en", "fr-Arab"],
});

const t_en = await i18n.loadMessages({ locale: "en-US" });
// Picks the "en" locale messages, since "en-US" and "en" both maximize to "en-Latn-US" and have the same script.
console.log(t_en("hello", { name: "Alice" }));
// Prints "Hello, Alice!"

const t_frArab = await i18n.loadMessages({ locale: "fr-Arab" });
console.log(t_frArab("hello", { name: "Alice" }));
// Prints "بونجور\u2066Alice\u2069!"

const t_fr = await i18n.loadMessages({ locale: "fr" });
// Throws error, since "fr" maximizes to "fr-Latn-FR" and no declared locales support Latin script in the French language.

📖 Usage Details

🗂 Message Structure

By convention, localized messages are stored under a common messages/ folder at the root of your project, with each locale stored under a nested folder named after the locale and common/namespaced messages stored in .json files under each locale folder.

For example, in a project that supports the "en" and "fr" locales and has localization messages under both a "common" and a "homepage" namespace, by default @websnacksjs/i18n expects to find the following directory structure:

messages/
  ├── en/
  │   ├── common.json
  │   └── homepage.json
  └──fr/
      ├── common.json
      └── homepage.json

@websnacksjs/i18n uses URL templates with :locale and :namespace placeholders to specify how to load messages given a particular locale and set of namespace files. By default, it uses the following message URL templates depending upon the runtime environment:

  • Server (node, Deno, etc.): file://${process.cwd()}/messages/:locale/:namespace.json
  • Browser: ///messages/:locale/:namespace.json

You can change this default behavior using the messagesUrlTemplate argment in I18n's constructor. For example, to tell @websnacksjs/i18n to fetch messages from your translation service at "https://translations.example.com/messages" without a ".json" extension:

import I18n from "@websnacksjs/i18n";

const i18n = new I18n({
 supportedLocales: ["en", "fr-Arab"],
 messagesUrlTemplate: "https://translations.example.com/messages/:locale/:namespace",
});

📄 Namespaces

@websnacksjs/i18n requires a common messages file that is always loaded when i18n.loadMessages(...) is called. As your application grows and you gather more and more locationalized messages, you may find some performance advantages to splitting up those messages into separate files that are only loaded for particular parts of your application (you should test this before commiting to this approach, as it adds some complexity and the performance benefit may be minor).

To use namespaces, simply store your namespaced messages files in a {namespace}.json file next to your common.json messsages file, replacing {namespace} with a short, readable name for your namespace. To load and use these namespaced message files, declare them in the I18n constructor and provide a namespaces parameter to i18n.loadMessages(...). Note that namespaced keys must be prefixed by "{namespace}:" (this is enforced at compile time, see TypeScript integration below):

./messages/en/common.json

{
 "hello": "Hello {{name}}!"
}

./messages/en/homepage.json

{
 "search catalog": "Search our catalog"
}
import I18n from "@websnacksjs/i18n";

const i18n = new I18n<{
 common: typeof import("./messages/en/common.json"),
 homepage: typeof import("./messages/en/homepage.json"),
}>({
 supportedLocales: ["en", "fr"],
 namespaces: ["homepage"],
});
const t = await i18n.loadMessages({ locale: "en", namespaces: ["homepage"] });
console.log(t("homepage:search catalog"));
// Prints "Search our catalog"

🪄 Locale Autodetection in Browsers

In the browser, the locale paramter of i18n.loadMessages(...) may be omitted to enable autodetection of the current user's locale. Autodetection works by first checking to see if the <html> tag has a valid lang attribute, and falls back to using navigator.languages if <html lang="..."> isn't present or valid.

🛡 TypeScript Integration & Type-Safe Keys

@websnacksjs/i18n supports strongly typed keys in TypeScript, turning typos into compile-time errors instead of poor experiences for end users. Strongly typed keys are automatically enabled with integration plugins such as @websnacksjs/i18n-astro, but if you're using a different framework or want to configure things manually you can leverage TypeScript's resolveJsonModule compiler option to inform the I18n class of the shape of your message files:

./messages/en/common.json

{
 "hello": "Hello {{name}}!"
}
import I18n from '@websnacksjs/i18n';

const i18n = new I18n<{
 common: typeof import("./messages/en/common.json"),
}>({
 supportedLocales: ["en", "fr"],
});
const t = await i18n.loadMessages({ locale: "en" });
console.log(t("welcome", { name: "Alice" }));
// Fails to compile: "welcome" is not a valid messages key.

If your translation files are not stored locally, you'll need to produce an appropriate TypeScript type from your source messages and pass that to I18n's constructor as a generic parameter.

Note that at present, messages with placeholders are not strongly typed and substitutions are not enforced by the compiler due to TypeScript issue #32063. Some type generation magic in integration libraries (e.g. @websnacksjs/i18n-astro) could provide a workaround, but such type generation has yet to be implemented. This means that the following code will compile but result in a runtime error:

import I18n from '@websnacksjs/i18n';

const i18n = new I18n({
 supportedLocales: ["en"],
});
const t = i18n.loadMessages({ locale: "en" });
console.log(t("hello"));
// Compiles, but throws an error at runtime: missing substitution for placeholder "{{name}}"

🌍 Standards Compliance & Locale Fallback

Only RFC 5646-compliant locale strings are supported in @websnacksjs/i18n to enable interoperability with standard libraries like Intl.

Enforcement of RFC 5646 in @websnacksjs/i18n also allows for zero-configuration locale fallback using Unicode CLDR's "Add Likely Subtags" alogorithm, where the region and/or script subtags of an abmiguous locale like "fr" can be inferred or "maximized" to "fr-Latn-FR". This allows @websnacksjs/i18n to make intelligent, safe fallback decisions, such as falling back to the "fr" locale (with latin script) when given an amgiuous locale like "fr-FR".

Note that in practice only fallbacks to locales that maximize to the same script and language are supported. A locale like "fr-Arab-MT" will NOT fallback to "fr" (maximized to "fr-Latn-FR") because that would result in a different script that end users may or may not be able to read.

🆚 Why @websnacksjs/i18n Instead of i18next?

| Feature | i18next | @websnacksjs/i18n | | ------------------------- | ------------------------------------- | ---------------------------------------------------- | | Missing key behavior | 濫 Outputs raw key |  Fails at build/runtime w/ Error | | Missing locale behavior | 濫 Render fallback locale |  Fails at build/runtime w/ Error | | Type-safe keys |  Requires configuration | ✨ Automatic (w/ astro integration) | | Setup Complexity |  Config-driven | ✨ Convention over configuration |

📜 License

@websnacksjs/i18n and associated integration libraies are all licensed under the Apache-2.0 license. See the LICENSE file for details.