@uengage.io/i18n
v0.5.0
Published
Per-business i18n helpers for uEngage (currency formatting today). Resolves a business's currency/country from the platform Business API and formats with the right symbol and separators.
Maintainers
Readme
@uengage.io/i18n
Per-business i18n helpers. Today: currency formatting with the business's symbol and separators, always 2 decimals.
import * as i18n from '@uengage.io/i18n';
const inst = await i18n.forBusinessId(businessId);
inst.currency.format(123456.5); // "₹1,23,456.50" (IN) · "$1,234.50" (CA) · "12 345,67 €" (PT)forBusinessId reads the public locale group of the platform Business Metadata Service
(GET /v1/businesses/:id?groups=locale, no token needed) through @uengage.io/platform-sdk.
Results are cached per business for 5 minutes; concurrent calls share one request; failures are not cached.
Setup
i18n.configure({
platform: { baseUrl: 'https://api.uat.platform.uengage.io' }, // default: UENGAGE_BASE_URL env, else prod
ttlMs: 300_000, // optional
});Calling configure is optional; without it the SDK env defaults apply.
API
| | |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| await forBusinessId(id) | Resolve a business, return an I18n instance. Throws BusinessApiError (e.g. 404) from the SDK. |
| forConfig({ currency, country?, locale?, timezone? }) | Same instance from config you already hold (sync). |
| inst.currency.format(amount, options?) | number or plain decimal string. Throws RangeError for NaN/Infinity/non-numeric or bad options. |
| inst.currency.code / .symbol / .locale | "INR", "₹", "en-IN" |
| inst.country / .timezone | ISO-2 or null; IANA zone or null |
| inst.time.formatDate / formatTime / formatDateTime(v) | "19 Oct 2026" / "10:05:07 pm" / both; more below. RangeError if unparseable. |
format options
| Option | Default | Notes |
| ------------ | ------------ | ---------------------------------------------------------------------------------------------------------- |
| notation | 'standard' | 'compact': en-IN ₹1.20 Lac / ₹3.00 Cr (full format below 1 lakh); others $1.2K, $3M, 1,2 mil € |
| decimals | 2 | Integer 0-10. In compact non-IN the default is Intl's ($1.2K); set it to fix digits ($1.23K). |
| showSymbol | true | false drops the currency symbol (1,234.50, 3.00 Cr). |
How the locale is chosen
addo_business.country is free text and mostly null, so the currency drives it:
- explicit
locale(forConfigonly) country, normalised ("Canada","CAN","CA"→CA)- the currency's home country:
INR→IN,CAD→CA,AED→AE,NZD→NZ,EUR→PT(Portugal is the only EUR market),USD→US IN
Country → locale: IN en-IN, CA en-CA, AE en-AE, NZ en-NZ, PT pt-PT, US en-US.
currency_code (legacy symbol) is ignored. A currency that is not a 3-letter code (e.g. "Rs.") is treated as INR.
Formatting is Intl.NumberFormat, so output follows the runtime's ICU. The PHP port (packages/i18n-php)
is tested against the same test/fixtures/currency-format.json.
How times are read
"2026-10-19 22:05:00"(no offset) is IST, the DB's storage convention, and is converted to the business timezone.- An ISO string with
Zor an offset, aDate, or epoch ms is an instant. "2026-10-19"(date parts) and"22:05"(formatTime,formatHour) are wall values, shown as is.timezonenull or not an IANA zone →Asia/Kolkata. Output isIntl.DateTimeFormatin the business locale.
| inst.time.* (en-IN · en-CA · pt-PT) | Intl options | Example |
| ------------------------------------- | ------------------------------------ | --------------------------------------------- |
| formatDate(v) | dateStyle: 'medium' | 19 Oct 2026 · Oct 19, 2026 · 19/10/2026 |
| formatTime(v) | timeStyle: 'medium' (with seconds) | 10:05:07 pm · 10:05:07 p.m. · 22:05:07 |
| formatDateTime(v) | both of the above | 19 Oct 2026, 10:05:07 pm |
| formatHour(v) | hour: 'numeric' | 10 pm · 10 p.m. · 22 |
| formatWeekday(v) | weekday: 'short', always English | Mon |
| formatDayMonth(v) | day: 'numeric', month: 'short' | 19 Oct · Oct 19 · 19/10 |
| formatMonthYear(v) | month: 'short', year: 'numeric' | Oct 2026 · Oct 2026 · 10/2026 |
Business day and instants
For logic that needs the outlet's clock rather than the browser's (Toronto shown, at 2026-10-23 08:00 IST):
| inst.time.* | Returns | Example |
| ------------------- | --------------------------------------------------------------------- | ----------------------------------------------- |
| toInstant(v) | Epoch ms of a DB value (naive = IST, ISO with offset as given) | "2026-10-22 22:05:07" → 1792686907000 |
| today(now?) | The outlet's current date | 2026-10-22 |
| startOfDay(date?) | IST DB lower bound of that outlet-local day (default today) | 2026-10-22 09:30:00 |
| endOfDay(date?) | IST DB upper bound, inclusive to the second; DST days are 23 or 25 h | 2026-10-23 09:29:59 |
| fromLocal(v) | IST DB value for a time the merchant entered in the outlet's timezone | "2026-10-22 10:00" → 2026-10-22 19:30:00 |
| toLocal(v) | A DB value as the outlet's local wall time (edit forms) | "2026-10-22 19:30:00" → 2026-10-22 10:00:00 |
DST: a local time skipped by DST moves forward (Toronto 2026-03-08 02:30 → 03:30 local); one that happens twice resolves to the first (Toronto 2026-11-01 01:30 → the EDT one). Day bounds follow the same rule, so a skipped midnight (America/Santiago, 2026-09-06) starts the day at the first real instant.
Develop
pnpm --filter @uengage.io/i18n test
pnpm --filter @uengage.io/i18n typecheck