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

@clustersoft/i18n

v1.0.0

Published

Universal, zero-dependency internationalisation for JavaScript and TypeScript — nested keys, namespaces, CLDR plural rules, Intl formatting and fallback chains, with optional ClusterSoft MGR bot integration.

Readme

@clustersoft/i18n

Universal internationalisation for JavaScript and TypeScript — zero dependencies, Intl-powered, and correct where hand-rolled helpers are not.

npm version types included node license

Use it anywhere you need translated text: an HTTP service, a CLI, a worker, a scheduled job, a test — or a ClusterSoft MGR bot, through an optional /bot entry point that the rest of the package knows nothing about.

  • 🟢 Zero runtime dependencies — nothing but the built-in Intl
  • 🌍 Runtime-agnostic core — no bot, no framework, no file system required
  • 🧠 Real pluralisation — Intl.PluralRules, so Russian one/few/many is right
  • 🧩 Two placeholder styles — {name} and {{name}}, mixed freely
  • 🗂 Nested, flat and namespaced keys — menu.main.title, menu_title, common:greeting
  • 📦 Files optional — pass message objects, or load a directory
  • 🔤 Intl formatting — numbers, currency, dates, relative time, inline in messages
  • 🧾 TypeScript-first — typed keys from your own message tree; plain JS works identically
  • 🤖 Optional bot layer — middleware, ctx.t, and a ready-made language picker

Features at a glance

| Area | What you get | |------|--------------| | Lookup | nested paths · flat keys · namespaces (common:greeting) · per-key fallback chain | | Plurals | CLDR categories via Intl.PluralRules · exact-count overrides ("0") · graceful degradation | | Interpolation | {name} · {{name}} · {{{escaped}}} · inline {amount, number} / {when, date} | | Formatting | number · currency · percent · date · time · dateTime · relativeTime · list | | Loading | plain objects · one file per locale · one folder per locale (namespaces) · runtime addLocale | | Storage | in-memory · Redis with your client · anything with get/set | | Bot layer | locale resolution per update · ctx.t / ctx.i18n · language keyboard + callback | | Packaging | ESM + CommonJS + .d.ts · dependencies: {} · Node ≥ 18 |


Table of contents


Installation

npm install @clustersoft/i18n

Requires Node.js ≥ 18. There are no runtime dependencies.

Two entry points:

| Import | Contents | |--------|----------| | @clustersoft/i18n | the engine — usable in any JS/TS project | | @clustersoft/i18n/bot | optional integration with @clustersoft/mgr-bot-sdk |

The SDK is an optional peer dependency: install it only if you use the second entry point.

Both module formats ship, so TypeScript, ESM and CommonJS behave identically:

// TypeScript / ES modules
import { I18n } from '@clustersoft/i18n'
// Plain JavaScript (CommonJS)
const { I18n } = require('@clustersoft/i18n')

Quick start

Create one engine for your process, then a translator wherever you render text.

TypeScript

import { I18n } from '@clustersoft/i18n'

const i18n = new I18n({
  defaultLocale: 'uz',
  fallbackLocale: ['uz', 'en'],
  locales: {
    uz: {
      welcome: 'Salom, {name}!',
      orders: { one: '{count} ta buyurtma', other: '{count} ta buyurtma' },
    },
    ru: {
      welcome: 'Привет, {name}!',
      orders: {
        one: '{count} заказ',
        few: '{count} заказа',
        many: '{count} заказов',
        other: '{count} заказа',
      },
    },
  },
})

const t = i18n.translator('ru')

t('welcome', { name: 'Eldor' })  // "Привет, Eldor!"
t('orders', { count: 1 })        // "1 заказ"
t('orders', { count: 3 })        // "3 заказа"
t('orders', { count: 5 })        // "5 заказов"

JavaScript

const path = require('node:path')
const { I18n } = require('@clustersoft/i18n')

const i18n = new I18n({
  defaultLocale: 'uz',
  fallbackLocale: ['uz', 'en'],
  directory: path.join(__dirname, 'locales'), // reads locales/*.json
})

// One bound translator per request keeps call sites short.
function handler(req, res) {
  const t = i18n.translator(req.query.lang)
  res.end(t('welcome', { name: 'Eldor' }))
}

Message files

Three ways to supply messages; combine them freely.

Plain objects — no file system involved, which is what you want when bundling or running serverless:

new I18n({ locales: { uz: { welcome: 'Salom!' }, ru: { welcome: 'Привет!' } } })

One file per locale — flat or nested keys:

locales/
├── uz.json
├── oz.json
├── ru.json
└── en.json
{
  "welcome": "Salom, {name}!",
  "menu": { "main": { "title": "Bosh menyu" } },
  "cart": { "items": { "one": "{count} ta mahsulot", "other": "{count} ta mahsulot" } }
}

A folder per locale — each file name becomes a namespace:

locales/
├── uz/
│   ├── common.json   → t('common:greeting')
│   └── menu.json     → t('menu:title')
└── ru/
    └── common.json
new I18n({ directory: './locales' }) // the layout is detected automatically

directory and locales can be used together — files load first, objects are deep-merged on top. addLocale() does the same at runtime, for lazily loaded or remotely fetched translations:

i18n.addLocale('en', await fetchTranslations('en'))
i18n.addLocale('en', invoiceMessages, 'invoice') // under a namespace

Keys, namespaces and fallbacks

i18n.t('uz', 'menu.main.title')   // nested path
i18n.t('uz', 'menu_title')        // flat key
i18n.t('uz', 'common:greeting')   // namespaced key

A key is resolved in this order:

  1. literally — a key written verbatim in the tree always wins, whichever separator it contains ({"user:name": "Ism"} resolves for t('user:name'), {"order.from": "Qayerdan"} for t('order.from'));
  2. as a namespace — ns:rest looks rest up inside the ns branch;
  3. as a nested path — menu.main.title walks the tree;
  4. in the default namespace (common, configurable with defaultNamespace) for keys with no ns: prefix.

Both separators are configurable (keySeparator, namespaceSeparator), and either can be set to false to switch that step off — see migrating from a hand-rolled helper.

Lookups walk a fallback chain: the requested locale, then every fallbackLocale in order, then defaultLocale.

const i18n = new I18n({ defaultLocale: 'uz', fallbackLocale: ['oz', 'en'] })

i18n.localeChain('ru')  // ['ru', 'oz', 'en', 'uz']

The chain is applied per key, not per locale — a locale that is only 40 % translated still renders fully, falling back key by key.


Interpolation

Both styles work, in the same message if you like:

{
  "hi": "Salom, {name}!",
  "bye": "Xayr, {{name}}!",
  "literal": "Use {{{name}}} for a placeholder"
}
i18n.t('uz', 'hi', { name: 'Eldor' })  // "Salom, Eldor!"
i18n.t('uz', 'literal')                // "Use {name} for a placeholder"

Messages can also format values inline:

{
  "balance": "Balansingiz: {amount, currency}",
  "total": "Jami {value, number} ta",
  "joined": "Sana: {when, date}",
  "price": "Narx: {value, currency, USD}"
}

Supported inline formats: number, currency (optional currency code), percent, date, time, datetime, relative, list. This is a deliberate "ICU-lite" subset — a readable shorthand for the common cases, not a full ICU message parser.


Pluralisation

Plural forms live under the key, using CLDR category names:

{
  "orders": {
    "0": "Нет заказов",
    "one": "{count} заказ",
    "few": "{count} заказа",
    "many": "{count} заказов",
    "other": "{count} заказа"
  }
}
i18n.t('ru', 'orders', { count: 1 })   // "1 заказ"
i18n.t('ru', 'orders', { count: 2 })   // "2 заказа"
i18n.t('ru', 'orders', { count: 5 })   // "5 заказов"
i18n.t('ru', 'orders', { count: 21 })  // "21 заказ"
i18n.t('ru', 'orders', { count: 0 })   // "Нет заказов"  (exact-count key)

Rules of the road:

  • the form is chosen by Intl.PluralRules for the locale of the message;
  • an exact-count key ("0", "1", …) beats the CLDR category;
  • count is interpolated as {count} automatically;
  • a partially translated set degrades to other, then many, then any form present — never to an empty string;
  • without a count, the neutral other form is used; no form is ever guessed.

Uzbek has a single plural form, so { "one": …, "other": … } with identical text is normal and correct — keeping both keys means the message stays valid if the wording ever diverges.


Formatting with Intl

const f = i18n.formatter('ru')

f.number(1234567)                       // "1 234 567"
f.currency(45000)                       // "45 000 UZS"  (defaultCurrency)
f.currency(1500, 'USD')                 // "1500 $"
f.percent(0.075)                        // "8 %"
f.date(new Date())                      // "15 янв. 2026 г."
f.time(new Date())                      // "09:30"
f.dateTime(new Date())                  // "15 янв. 2026 г., 09:30"
f.relativeTime(Date.now() - 3_600_000)  // "1 час назад"
f.relativeTime(-2, 'day')               // "2 дня назад"
f.list(['a', 'b', 'c'])                 // "a, b и c"

i18n.format is the same object bound to the default locale. Every helper accepts the matching native Intl options as a last argument, and falls back to String(value) instead of throwing when a runtime lacks data for a locale.


Missing keys and variables

Nothing renders blank. A key missing from every locale in the chain is returned as-is, and reported:

const i18n = new I18n({
  onMissing: ({ key, locale, chain }) => {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(`[i18n] missing ${locale}:${key} (tried ${chain.join(' → ')})`)
    }
    // return a string here to override what t() gives back
  },
})

A placeholder whose parameter was not supplied is left in place — visible rather than silently empty — and reported the same way:

new I18n({ onMissingVar: ({ name }) => `«${name}»` })

Storing a user's locale

The engine itself is stateless. When you need to remember a choice, use a store — two methods, and the package imports no client of its own:

interface LocaleStore {
  get(key: string, context?: unknown): string | null | undefined | Promise<…>
  set(key: string, locale: string, context?: unknown): void | Promise<void>
  delete?(key: string, context?: unknown): void | Promise<void>
}

| Store | Behaviour | |-------|-----------| | MemoryLocaleStore | per-process map; fine for a single instance | | RedisLocaleStore | pass your ioredis-compatible client; optional TTL | | SessionLocaleStore | from /bot — keeps the locale in ctx.session | | yours | anything implementing the two methods |

import Redis from 'ioredis'
import { RedisLocaleStore } from '@clustersoft/i18n'

const store = new RedisLocaleStore(new Redis(process.env.REDIS_URL), {
  ttlSeconds: 60 * 60 * 24 * 365,
})

await store.set('user:42', 'ru')
const t = i18n.translator((await store.get('user:42')) ?? 'uz')

TypeScript

Typed keys come from your own message tree — no code generation step:

import { I18n, defineLocale } from '@clustersoft/i18n'

const uz = defineLocale({
  welcome: 'Salom, {name}!',
  menu: { main: { title: 'Bosh menyu' } },
})

const i18n = new I18n<typeof uz>({ locales: { uz }, defaultLocale: 'uz' })

i18n.t('uz', 'menu.main.title')  // autocompleted
i18n.t('uz', someDynamicKey)     // still allowed — no cast required

defineLocale is an identity function; it exists only to preserve the literal shape of the object. An imported JSON file works as the type source too (import uz from './locales/uz.json' with resolveJsonModule).


Bot integration (optional)

Everything above is bot-free. If you are building on @clustersoft/mgr-bot-sdk, the /bot entry point adds per-update locale resolution, ctx.t, and the language picker.

Why a language picker is mandatory on MGR

MGR updates identify the sender but carry no language field — a bot cannot detect a user's language automatically. The practical consequence: ask once, then remember. This package treats that flow as a first-class feature rather than an afterthought. If the platform ever starts sending a user language, wire it in through the localeFromUpdate hook without touching the rest of your bot.

TypeScript

import { Bot } from '@clustersoft/mgr-bot-sdk'
import { I18n } from '@clustersoft/i18n'
import { BotI18n } from '@clustersoft/i18n/bot'
import '@clustersoft/i18n/bot/augment' // types for ctx.t / ctx.i18n

const i18n = new I18n({ defaultLocale: 'uz', directory: './locales' })
const botI18n = new BotI18n(i18n)

const bot = new Bot(process.env.BOT_TOKEN!, {
  apiRoot: 'https://api.clustersoft.uz/api/v1/botapi',
})

bot.session()                  // register first: the locale rides in the session
bot.use(botI18n.middleware())  // then: ctx.t and ctx.i18n become available

bot.start((ctx) => ctx.reply(ctx.t('welcome', { name: 'Eldor' })))
bot.command('lang', (ctx) =>
  ctx.reply(ctx.t('choose_language'), ctx.i18n.languageKeyboard({ columns: 2 })),
)

botI18n.handleLanguageCallback(bot) // handles every `locale:<code>` button

bot.launch()

JavaScript

const { Bot } = require('@clustersoft/mgr-bot-sdk')
const { I18n } = require('@clustersoft/i18n')
const { BotI18n } = require('@clustersoft/i18n/bot')

const i18n = new I18n({ defaultLocale: 'uz', directory: './locales' })
const botI18n = new BotI18n(i18n)
const bot = new Bot(process.env.BOT_TOKEN, { apiRoot: '…/api/v1/botapi' })

bot.session()
bot.use(botI18n.middleware())
bot.start((ctx) => ctx.reply(ctx.t('welcome', { name: 'Eldor' })))
botI18n.handleLanguageCallback(bot)
bot.launch()

Locale resolution per update

First match wins:

| # | Source | Typical use | |---|--------|-------------| | 1 | resolveLocale(ctx) | your own database or account settings | | 2 | store.get(key, ctx) | the persisted choice (session, Redis, …) | | 3 | ctx.session[sessionKey] | a session written by another middleware | | 4 | localeFromUpdate(ctx) | reserved for a future platform-supplied language | | 5 | i18n.defaultLocale | the guaranteed fallback |

const botI18n = new BotI18n(i18n, {
  resolveLocale: async (ctx) => db.getUserLang(ctx.from?.id),
  getLocaleKey: (ctx) => `${ctx.from?.id}`,   // default: "<chatId>:<fromId>"
  store: new RedisLocaleStore(redis),         // default: SessionLocaleStore
})

ctx.i18n.setLocale('ru') switches the locale for the rest of the update and persists the choice.

The language picker

bot.command('lang', (ctx) =>
  ctx.reply(ctx.t('choose_language'), ctx.i18n.languageKeyboard()),
)

botI18n.handleLanguageCallback(bot, {
  onChanged: (ctx) => ctx.editMessageText(ctx.t('welcome', { name: 'Eldor' })),
})

languageKeyboard() renders one button per loaded locale, each label written in its own language (🇺🇿 O'zbekcha, 🇺🇿 Ўзбекча, 🇷🇺 Русский, 🇬🇧 English), and emits locale:<code> when pressed; ctx.i18n.languageKeyboard() marks the active locale. The handler stores the choice, acknowledges the press with your language_changed message, then calls onChanged.

Not using the SDK's router? botI18n.languageMiddleware() does the same job as a middleware.

The language registry (uz, oz, ru, en) lives in the core entry point, because native language names are just as useful in a web settings page. Extend or override it:

new I18n({
  languages: {
    kk: { nativeName: 'Қазақша', flag: '🇰🇿', intlLocale: 'kk-KZ' },
    en: { flag: '🇺🇸' }, // override a single field
  },
})

Language codes: uz vs oz

Across the ClusterSoft ecosystem (and the MGR bots that follow it):

| Code | Script | Native name | Intl tag | |------|--------|-------------|-----------| | uz | Latin | O'zbekcha | uz-Latn-UZ | | oz | Cyrillic | Ўзбекча | uz-Cyrl-UZ |

oz is not a valid BCP 47 tag, so the registry maps it to uz-Cyrl-UZ before handing it to Intl — plural rules and formatting stay correct. Any custom code you add can carry its own intlLocale the same way.


Migrating from a hand-rolled helper

Most projects start with something like this:

// before — src/core/i18n.js
const uz = require('./locales/uz.json')
const ru = require('./locales/ru.json')

const dict = { uz, ru }

function t(lang, key, params) {
  let s = dict[lang]?.[key] ?? dict.uz[key] ?? key
  for (const [k, v] of Object.entries(params ?? {})) {
    s = s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))
  }
  return s
}

The replacement keeps the same call shape, so call sites do not change:

// after
const { I18n } = require('@clustersoft/i18n')
const uz = require('./locales/uz.json')
const ru = require('./locales/ru.json')

const i18n = new I18n({ defaultLocale: 'uz', locales: { uz, ru } })

const t = (lang, key, params) => i18n.t(lang, key, params)

What you gain immediately, with the JSON files untouched:

| Concern | Before | After | |---------|--------|-------| | Plural forms | "5 заказа" | Intl.PluralRules per locale | | Nested keys | flat only | menu.main.title (flat keys still work) | | Fallbacks | one hard-coded locale | ordered chain, applied per key | | Missing keys | silent | onMissing hook | | Numbers, dates, money | manual | Intl helpers and inline formats | | Adding a locale | edit the dictionary map | drop a file into locales/ |

Existing keys keep working as written: a flat key is always matched literally first, so "order.from" and "user:name" resolve verbatim even though . and : are the nested-path and namespace separators.

If your catalogue is entirely flat and you want to rule out any splitting at all, turn both separators off — the strictest, most predictable migration mode:

const i18n = new I18n({
  defaultLocale: 'uz',
  locales: { uz, ru },
  keySeparator: false,       // no `menu.main.title` walking
  namespaceSeparator: false, // no `common:greeting` splitting
})

Every key is then looked up exactly as written, which makes the migration a pure drop-in. Turn the separators back on whenever you decide to nest.

Then migrate at your own pace: replace t(lang, …) with a bound i18n.translator(lang), convert plural strings into form objects, and — in a bot — swap the manual language menu for languageKeyboard().


API reference

new I18n(options?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | defaultLocale | string | 'uz' | Locale used when nothing else resolves | | fallbackLocale | string \| string[] | defaultLocale | Locales searched, in order, for a missing key | | locales | Record<string, Messages> | — | Message trees supplied directly (no file system) | | directory | string | — | Directory of *.json message files | | namespaces | boolean | auto | Force namespaced lookups (auto-detected from the layout) | | defaultNamespace | string | 'common' | Namespace used for unprefixed keys | | namespaceSeparator | string \| false | ':' | Separator between namespace and key; false disables namespace splitting | | keySeparator | string \| false | '.' | Separator between nested key segments; false disables nesting | | onMissing | (info) => string \| void | — | Missing-key hook; may supply the return value | | onMissingVar | (info) => string \| void | — | Missing-parameter hook | | languages | Record<string, Partial<LanguageInfo>> | — | Extends/overrides the language registry | | defaultCurrency | string | 'UZS' | Currency used by format.currency(v) |

I18n members

| Member | Returns | Description | |--------|---------|-------------| | t(locale, key, params?) | string | Translate a key | | translator(locale) | (key, params?) => string | Translate function bound to a locale | | has(locale, key) | boolean | Key exists in that locale (fallbacks excluded) | | locales | string[] | Every loaded locale code | | localeChain(locale) | string[] | The fallback chain for a locale | | addLocale(locale, messages, namespace?) | this | Add/deep-merge messages at runtime | | getMessages(locale) | Messages \| undefined | The raw message tree | | format | Formatter | Intl helpers for the default locale | | formatter(locale) | Formatter | Intl helpers for a locale | | intlLocale(locale) | string | The BCP 47 tag a code maps to | | defaultLocale · fallbackLocales · languages · options | — | The resolved configuration |

Formatter

| Method | Example output (ru) | |--------|----------------------| | number(v, opts?) | 1 234 567 | | currency(v, code?, opts?) | 45 000 UZS | | percent(v, opts?) | 8 % | | date(v, opts?) | 15 янв. 2026 г. | | time(v, opts?) | 09:30 | | dateTime(v, opts?) | 15 янв. 2026 г., 09:30 | | relativeTime(v, unit?, opts?) | 3 часа назад | | list(values, opts?) | a, b и c | | intlLocale | the BCP 47 tag these helpers are bound to |

Core exports

| Export | Description | |--------|-------------| | I18n | The translation engine | | defineLocale(messages) | Identity helper preserving the literal message shape | | MemoryLocaleStore · RedisLocaleStore | Ready-made locale stores | | LANGUAGES · buildLanguageRegistry · intlLocaleOf · languageLabel | Language registry | | interpolate · pluralCategory · selectPluralForm · isPluralForms | Low-level building blocks | | createFormatter(intlLocale, currency?) | Intl helpers for any BCP 47 tag | | loadLocalesFromDirectory · mergeMessages · lookupPath | Loader internals | | types | Messages · PluralForms · LocaleStore · LanguageInfo · I18nOptions · Translator · … |

new BotI18n(i18n, options?) — from @clustersoft/i18n/bot

| Option | Type | Default | Description | |--------|------|---------|-------------| | store | LocaleStore | SessionLocaleStore | Where the chosen locale is persisted | | getLocaleKey | (ctx) => string \| undefined | "<chatId>:<fromId>" | Store key for an update | | resolveLocale | (ctx) => string \| undefined \| Promise<…> | — | Highest-priority locale source | | localeFromUpdate | (ctx) => string \| undefined | — | Locale carried by the update itself | | sessionKey | string | 'locale' | Property used on ctx.session | | languagePrefix | string | 'locale:' | Callback-data prefix of the language keyboard |

| Member | Returns | Description | |--------|---------|-------------| | middleware() | Middleware | Resolves the locale, attaches ctx.t / ctx.i18n | | languageKeyboard(opts?) | LanguageKeyboard | Inline keyboard of language buttons | | handleLanguageCallback(bot, opts?) | this | Registers the locale:<code> handler | | languageMiddleware(opts?) | Middleware | Middleware form of that handler | | resolveLocale(ctx) | Promise<string> | The resolution chain, callable directly | | persistLocale(ctx, locale) | Promise<void> | Writes the choice to the store | | i18n · store · options | — | The bound engine and configuration |

ctx.i18n (per update)

| Member | Description | |--------|-------------| | locale | The locale resolved for this update | | locales | Every loaded locale code | | t(key, params?) | Translate in the current locale (same as ctx.t) | | translate(locale, key, params?) | Translate in an explicit locale | | setLocale(locale) | Switch for this update and persist | | format | Intl helpers bound to the current locale | | languageKeyboard(opts?) | Language keyboard with the current locale marked | | engine | The I18n instance |

Bot exports

| Export | Description | |--------|-------------| | BotI18n · botI18n(i18n, opts?) | The binding class and its factory | | SessionLocaleStore · defaultLocaleKey | Session-backed store and the default key builder | | languageKeyboard · parseLanguageCallback | Standalone picker helpers | | types | ContextI18n · BotI18nOptions · BotContextLike · LanguageKeyboard · … |


Examples

| File | Shows | |------|-------| | examples/standalone.js | The engine alone, plain CommonJS, every locale | | examples/service.js | A Node HTTP service: one engine, a translator per request | | examples/plural.ts | Russian plural forms and Intl formatting | | examples/typed-keys.ts | Typed keys and inline formats | | examples/bot-echo.ts · bot-echo.js | A translated bot, in TypeScript and JavaScript | | examples/bot-language-picker.ts | Asking for a language and remembering it |

npm run build
node examples/standalone.js
PORT=3457 node examples/service.js

License

MIT © ClusterSoft

02.00-i18n-tool-x