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

@robojs/i18n

v0.1.1

Published

Safely translate your Robo and Discord slash commands.

Readme


@robojs/i18n

Type-safe i18n for Robo.js with MessageFormat 2 (MF2).

Drop JSON files in /locales, get strongly-typed namespaced keys & parameters, and format messages at runtime with t() or the strict tr()—no custom build steps required.

  • Strong types from your JSON — MF2 infers keys & param types.
  • Runtime formatting anywhere (Discord & beyond) with t(), strict tr(), and withLocale.
  • Arrays supported — string-array messages return a fully formatted string[].
  • Zero-friction & fast — drop /locales/**.json; loads once with a tiny formatter cache.

📚 Documentation: Getting started

🚀 Community: Join our Discord server

Installation 💻

Add the plugin to an existing Robo:

npx robo add @robojs/i18n

Or start a new project with it preinstalled:

npx create-robo <project-name> -p @robojs/i18n

Folder structure

Put message files under /locales/<locale>/**/*.json.

Keys are automatically namespaced from the file path:

  • /locales/<locale>/app.json ⇒ prefix app:
  • /locales/<locale>/shared/common.json ⇒ prefix shared/common:
  • Deeper paths keep slash-separated folders + filename (no .json), then :
    • e.g. /locales/en-US/marketing/home/hero.jsonmarketing/home/hero:

Example tree:

/locales
  /en-US
    app.json
  /es-ES
    app.json

en-US/app.json

{
	"hello": "Hello {$name}!",
	"pets.count": ".input {$count :number}\n.match $count\n  one {{You have {$count} pet}}\n  *   {{You have {$count} pets}}"
}

es-ES/app.json

{
	"hello": "¡Hola {$name}!",
	"pets.count": ".input {$count :number}\n.match $count\n  one {{Tienes {$count} mascota}}\n  *   {{Tienes {$count} mascotas}}"
}

Only string values are used. Non-JSON files are ignored. The plugin loads everything once, keeps it in state, and generates types from what it finds.

Runtime usage (t) 🌐

t(localeLike, key, params?) formats a message right now. The params type is inferred from key.

import { t } from '@robojs/i18n'

// Accepts a string, a Discord Interaction, or any { locale } / { guildLocale } object
const locale = 'en-US' as const

t(locale, 'app:hello', { name: 'Robo' }) // "Hello Robo!"
t(locale, 'app:pets.count', { count: 3 }) // "You have 3 pets"

locale can be:

  • 'en-US' (string)
  • { locale: 'en-US' }
  • { guildLocale: 'en-US' }

Strict runtime usage (tr) 🔒

tr(localeLike, key, ...args) is a strict variant of t:

  • If the message has parameters, they are required and non-undefined.
  • If the message has no parameters, you can omit the params object.
import { tr } from '@robojs/i18n'

const locale = 'en-US' as const

tr(locale, 'app:hello', { name: 'Robo' }) // ✅ required
// tr(locale, 'app:hello')                // ❌ compile-time error

tr(locale, 'app:ping') // ✅ key with no params

Cleaner calls with withLocale 🧼

Avoid threading locale around:

import { withLocale } from '@robojs/i18n'
import type { ChatInputCommandInteraction } from 'discord.js'

export default (interaction: ChatInputCommandInteraction) => {
	const t$ = withLocale(interaction)
	return t$('app:hello', { name: 'Robo' })
}

Strict variant

Pass { strict: true } to get a curried strict translator (tr$) that enforces required params like tr:

import { withLocale } from '@robojs/i18n'

const tr$ = withLocale('en-US', { strict: true })
tr$('app:hello', { name: 'Robo' }) // ✅ required
// tr$('app:hello')                 // ❌ compile-time error

tr$('app:ping') // ✅ key with no params

Nesting 🧩

You can nest keys inside JSON and provide nested objects for params.

Nested keys

Instead of writing flat keys, you may structure your locale file:

// /locales/en-US/app.json
{
	"greetings": {
		"hello": "Hello {$name}!"
	}
}

The key becomes app:greetings.hello.

⚠️ Collision rule: a file cannot contain both a literal dotted key and a nested object that flatten to the same key (e.g., "greetings.hello": "…" and { "greetings": { "hello": "…" } }). The build will error with a clear message.

Nested parameter objects

Although MF2 placeholders look flat ({$user.name}), you can pass nested objects and they’ll be flattened for you.

// /locales/en-US/app.json
{
	"profile": "Hi {$user.name}! You have {$stats.count :number} points."
}
t('en-US', 'app:profile', {
	user: { name: 'Robo' },
	stats: { count: 42 }
})
// -> "Hi Robo! You have 42 points."

Prefer objects for readability; dotted param keys like { 'user.name': 'Robo' } also work if you need them.

Arrays

Locale values can be arrays of strings. Each element is formatted with MF2, and t()/tr() return a string[] for that key.

/locales/en-US/shared/common.json

{
	"arr": ["One {$n :number}", "Two"]
}

/locales/es-ES/shared/common.json

{
	"arr": ["Uno {$n :number}", "Dos"]
}
t('en-US', 'shared/common:arr', { n: 7 }) // ["One 7", "Two"]
t('es-ES', 'shared/common:arr', { n: 3 }) // ["Uno 3", "Dos"]

Arrays support MF2 placeholders per element, including :number, :date, :time, and :datetime. Non-string arrays are ignored.

Discord slash commands

createCommandConfig 🎮

Import createCommandConfig from @robojs/i18n instead of robo.js to define slash command metadata with i18n keys. The plugin will fill in names and descriptions for all locales at runtime.

import { createCommandConfig, t } from '@robojs/i18n'
import type { ChatInputCommandInteraction } from 'discord.js'
import type { CommandOptions } from 'robo.js'

export const config = createCommandConfig({
	nameKey: 'commands:ping.name',
	descriptionKey: 'commands:ping.desc',
	options: [
		{
			type: 'string',
			name: 'text',
			nameKey: 'commands:ping.arg.name',
			descriptionKey: 'commands:ping.arg.desc'
		}
	]
} as const)

export default (interaction: ChatInputCommandInteraction, options: CommandOptions<typeof config>) => {
	const user = { name: options.text ?? 'Robo' }
	return t(interaction, 'commands:hey', { user })
}

/locales/en-US/commands.json

{
	"hey": "Hey there, {$user.name}!",
	"ping": {
		"name": "ping",
		"desc": "Measure latency",
		"arg": {
			"name": "text",
			"desc": "Optional text to include"
		}
	}
}

For options, name is still required (helps TS inference) and should be provided alongside nameKey.

Performance ⚡

We keep a small in-memory cache of compiled MessageFormat instances, keyed by (locale, key, message). This avoids reparsing strings on repeated calls and fits apps with a few hundred keys per locale. You can clear it during tests or hot-reload:

import { clearFormatterCache } from '@robojs/i18n'
clearFormatterCache()

CLI (i18n) 🧰

This package ships a tiny CLI to build & refresh locale types and caches.

  • Run once (auto-detects your /locales and generates types):
npx i18n

Example output:

i18n:ready - Locales built in 3ms
  • Project binary: after install, you can also call:
pnpm i18n
# or
npm run i18n

The CLI is published under the i18n binary (see bin in package.json). It’s safe to run in CI before builds.

Supported MF2 pieces (what’s parsed)

| MF2 element | Example snippet | Param type inferred | | ----------- | ---------------------------------------------------------------- | ------------------- | | variable | {$name} | string | | number | {$count :number} | number | | match (num) | .input {$count :number}\n.match $count\n one {{…}}\n * {{…}} | number | | date | {$ts :date} | Date \| number | | time | {$ts :time} | Date \| number | | datetime | {$ts :datetime} | Date \| number |

If different locales disagree on a param’s kind, the type safely widens (e.g., number vs stringstring; any date/time → Date \| number).

How type-safety works

On first load, the plugin:

  1. Scans /locales/**.json.
  2. Parses MessageFormat 2 messages to detect parameter kinds (number/date/time/variable usage).
  3. Emits generated/types.d.ts with:
    • type Locale = 'en-US' | 'es-ES' | ...
    • type LocaleKey = 'app:hello' | 'app:pets.count' | 'shared/common:...' | ...namespaced
    • type LocaleParamsMap and type ParamsFor<K>

Formatting is done by messageformat at runtime, with a small cache of compiled formatters to reduce CPU work across calls.

Notes & FAQs

  • Works outside Discordt()/tr() are plain functions. Use them anywhere you can pass a locale string or object.
  • Missing locale or key → throws an error (fast fail).
  • Nested MF2 (e.g., .match blocks) is traversed correctly.
  • No manual type imports neededt()/tr() infer ParamsFor<K> from your keys.
  • Namespaced keys are required — always use the <folders>/<file>: prefix (e.g., app:hello, shared/common:greet).

Got questions? 🤔

If you have any questions or need help with this plugin, join our Discord — we’re friendly and happy to help!

🚀 Community: Join our Discord server