Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/fnando/i18n

A small library to provide the I18n translations on the JavaScript.
https://github.com/fnando/i18n

i18n internationalization javascript localization typescript

Last synced: 14 days ago
JSON representation

A small library to provide the I18n translations on the JavaScript.

Awesome Lists containing this project

README

        


i18n.js


A small library to provide the i18n translations on the JavaScript.


Tests
npm version
npm downloads
License: MIT

## Installation

- Yarn: `yarn add i18n-js@latest`
- NPM: `npm install i18n-js@latest`

## Usage

### Setting up

First, you need to instantiate `I18n` with the translations' object, the main
class of this library.

```js
import { I18n } from "i18n-js";
import translations from "./translations.json";

const i18n = new I18n(translations);
```

The `translations` object is a direct export of translations defined by
[Ruby on Rails](https://guides.rubyonrails.org/i18n.html). To export the
translations, you can use [i18n-js](https://github.com/fnando/i18n-js), a Ruby
gem that's completely disconnected from Rails and that can be used for the
solely purpose of exporting the translations, even if your project is written in
a different language. If all you care about is some basic translation mechanism,
then you can set the object like this:

```js
const i18n = new I18n({
en: {
hello: "Hi!",
},
"pt-BR": {
hello: "Olá!",
},
});
```

Each root key is a different locale that may or may not have the script code.
This library also supports locales with region code, like `zh-Hant-TW`.

Once everything is set up, you can then define the locale. `en` is both the
current and default locale. To override either values, you have to use
`I18n#defaultLocale` and `I18n#locale`.

```js
i18n.defaultLocale = "pt-BR";
i18n.locale = "pt-BR";
```

#### Base translations

This library comes bundled with all base translations made available by
[rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale).
Base translations allow formatting date, numbers, and sentence connectors, among
other things.

To load the base translations, use something like the following:

```js
import { I18n } from "i18n-js";
import ptBR from "i18n-js/json/pt-BR.json";
import en from "i18n-js/json/en.json";

const i18n = new I18n({
...ptBR,
...en,
});
```

### Updating translation store

Updating the translation store is trivial. All you have to do is calling
`I18n#store` with the translations that need to be merged. Let's assume you've
exported all your app's translations using
[i18n-js](https://github.com/fnando/i18n-js) CLI, using a separate file for each
language, like this:

- `translations/en.json`
- `translations/pt-BR.json`

This is how you could update the store:

```js
import { I18n } from "i18n-js";
import ptBR from "translations/pt-BR.json";
import en from "translations/en.json";

const i18n = new I18n();

i18n.store(en);
i18n.store(ptBR);
```

This method will allow you to lazy load translations and them updating the store
as needed.

```js
import { I18n } from "i18n-js";

async function loadTranslations(i18n, locale) {
const response = await fetch(`/translations/${locale}.json`);
const translations = await response.json();

i18n.store(translations);
}

const i18n = new I18n();
loadTranslations(i18n, "es");
```

### Events

A change event is triggered whenever `I18n#store` or `I18n#update` is called, or
when `I18n#locale`/`I18n#defaultLocale` is set. To subscribe to these changes,
use the method `I18n#onChange(i18n: I18n)`.

```js
const i18n = new I18n();
i18n.onChange(() => {
console.log("I18n has changed!");
});
```

Every change will increment the property `I18n#version`, so you can use it as a
cache key. Also, when you subscribe to change events,
`I18n#onChange(i18n: I18n)` will return another function that can be used to
remove the event handler.

```js
useEffect(() => {
const unsubscribe = i18n.onChange(() => {
// do something
});

return unsubscribe;
}, []);

useEffect(() => {
console.log("I18n has been updated!");
}, [i18n.version]);
```

### Translating messages

To translate messages, you have to use the `I18n#translate`, or its `I18n#t`
alias.

```js
i18n.locale = "en";
i18n.t("hello"); //=> Hi!

i18n.locale = "pt-BR";
i18n.t("hello"); //=> Olá!
```

You can also provide an array as scope. Both calls below are equivalent.

```js
i18n.t(["greetings", "hello"]);
i18n.t("greetings.hello");
```

Your translations may have dynamic values that should be interpolated. Here's a
greeting message that takes a name:

```js
const i18n = new I18n({
en: { greetings: "Hi, %{name}!" },
"pt-BR": { greetings: "Olá, %{name}!" },
});

i18n.t("greetings", { name: "John" });
```

If the translation is an array and the entry is a string, values will be
interpolated in a shallow way.

```js
const i18n = new I18n({
en: { messages: ["Hello there!", "Welcome back, %{name}!"] },
});

i18n.t("messages", { name: "John" });
//=> ["Hello there!", "Welcome back, John!"]
```

You may want to override the default
[`interpolate`](https://github.com/fnando/i18n/blob/main/src/helpers/interpolate.ts)
function with your own, if for instance you want these dynamic values to be
React elements:

```jsx
const i18n = new I18n({
en: { greetings: "Hi, %{name}!" },
"pt-BR": { greetings: "Olá, %{name}!" },
});

i18n.interpolate = (i18n, message, options) => {
// ...
};

return {i18n.t("greetings", { name: John })};
```

#### Missing translations

A translation may be missing. In that case, you may set the default value that's
going to be returned.

```js
i18n.t("missing.scope", { defaultValue: "This is a default message" });
```

Default messages can also have interpolation.

```js
i18n.t("noun", { defaultValue: "I'm a {{noun}}", noun: "Mac" });
```

Alternatively, you can define a list of scopes that will be searched instead.

```js
// As a scope
i18n.t("some.missing.scope", { defaults: [{ scope: "some.existing.scope" }] });

// As a simple translation
i18n.t("some.missing.scope", { defaults: [{ message: "Some message" }] });
```

Default values must be provided as an array of objects where the key is the type
of desired translation, a `scope` or a `message`. The returned translation will
be either the first scope recognized, or the first message defined.

The translation will fall back to the `defaultValue` translation if no scope in
`defaults` matches and if no `message` default is found.

You can enable translation fallback with `I18n#enableFallback`.

```js
i18n.enableFallback = true;
```

By default missing translations will first be looked for in less specific
versions of the requested locale and if that fails by taking them from your
`I18n#defaultLocale`.

```js
// if i18n.defaultLocale = "en" and translation doesn't exist
// for i18n.locale = "de-DE" this key will be taken from "de" locale scope
// or, if that also doesn't exist, from "en" locale scope
i18n.t("some.missing.scope");
```

Custom fallback rules can also be specified for a specific language. The locale
handler must be registered using `i18n.locales.register()`.

```js
i18n.locales.register("no", ["nb", "en"]);
```

By default a missing translation will be displayed as
`[missing "name of scope" translation]`. You can override this behavior by
setting `i18n.missingBehavior` to `"guess"`.

```js
i18n.missingBehavior = "guess";
```

The "guess" behavior will take the last section of the scope and apply some
replace rules; camel case becomes lower case and underscores are replaced with
space. In practice, it means that a scope like
`questionnaire.whatIsYourFavorite_ChristmasPresent` becomes
`what is your favorite Christmas present`.

There's also a strategy called `error`, which will throw an exception every time
you fetch a missing translation. This is great for development. It'll even end
up on your error tracking!

```js
i18n.missingBehavior = "error";
```

To detect missing translations, you can also set
`i18n.missingTranslationPrefix`.

```js
i18n.missingTranslationPrefix = "EE: ";
```

The same `questionnaire.whatIsYourFavorite_ChristmasPresent` scope would
converted into `EE: what is your favorite Christmas present`. This is helpful if
you want to add a check to your automated tests.

If you need to specify a missing behavior just for one call, you can provide a
custom `missingBehavior` option.

```js
i18n.t("missing.key", { missingBehavior: "error" });
```

You can completely override the missing translation strategy by setting it to a
function. The following example will return `null` for every missing
translation.

```js
i18n.missingTranslation = () => null;
```

Finally, you can also create your own missing translation behavior. The example
below registers a new behavior that returns an empty string in case a
translation is missing.

```js
i18n.missingTranslation.register("empty", (i18n, scope, options) => "");
```

#### Pluralization

This library has support for pluralization and by default works with English,
and similar pluralized languages like Portuguese.

First, you have to define your translations with special keywords defined by the
pluralization handler. The default keywords are `zero`, `one`, and `other`.

```js
const i18n = new I18n({
en: {
inbox: {
zero: "You have no messages",
one: "You have one message",
other: "You have %{count} messages",
},
},

"pt-BR": {
inbox: {
zero: "Você não tem mensagens",
one: "Você tem uma mensagem",
other: "Você tem %{count} mensagens",
},
},
});
```

To retrieve the pluralized translation you must provide the `count` option with
a numeric value.

```js
i18n.t("inbox", { count: 0 }); //=> You have no messages
i18n.t("inbox", { count: 1 }); //=> You have one message
i18n.t("inbox", { count: 2 }); //=> You have 2 messages
```

You may need to define new rules for other languages like Russian. This can be
done by registering a handler with `i18n.pluralization.register()`. The
following example defines a Russian pluralizer.

```js
i18n.pluralization.register("ru", (_i18n, count) => {
const mod10 = count % 10;
const mod100 = count % 100;
let key;

const one = mod10 === 1 && mod100 !== 11;
const few = [2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100);
const many =
mod10 === 0 ||
[5, 6, 7, 8, 9].includes(mod10) ||
[11, 12, 13, 14].includes(mod100);

if (one) {
key = "one";
} else if (few) {
key = "few";
} else if (many) {
key = "many";
} else {
key = "other";
}

return [key];
});
```

You can find all rules on
[http://www.unicode.org/](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html).

You can also leverage [make-plural](https://github.com/eemeli/make-plural/),
rather than writing all your pluralization functions. For this, you must wrap
make-plural's function by using
`useMakePlural({ pluralizer, includeZero, ordinal })`:

```js
import { ru } from "make-plural";
import { useMakePlural } from "i18n-js";

i18n.pluralization.register("ru", useMakePlural({ pluralizer: ru }));
```

#### Other options

If you're providing the same scope again and again, you can reduce the
boilerplate by setting the `scope` option.

```js
const options = { scope: "activerecord.attributes.user" };

i18n.t("name", options);
i18n.t("email", options);
i18n.t("username", options);
```

### Number Formatting

Similar to Rails helpers, you can have localized number and currency formatting.

```js
i18n.l("currency", 1990.99);
// $1,990.99

i18n.l("number", 1990.99);
// 1,990.99

i18n.l("percentage", 123.45);
// 123.450%
```

To have more control over number formatting, you can use the
`I18n#numberToHuman`, `I18n#numberToPercentage`, `I18n#numberToCurrency`,
`I18n#numberToHumanSize`, `I18n#numberToDelimited` and `I18n#numberToRounded`
functions.

#### `I18n#numberToCurrency`

Formats a `number` into a currency string (e.g., $13.65). You can customize the
format in the using an `options` object.

The currency unit and number formatting of the current locale will be used
unless otherwise specified in the provided options. No currency conversion is
performed. If the user is given a way to change their locale, they will also be
able to change the relative value of the currency displayed with this helper.

##### Options

- `precision` - Sets the level of precision (defaults to 2).
- `roundMode` - Determine how rounding is performed (defaults to `default`.)
- `unit` - Sets the denomination of the currency (defaults to "$").
- `separator` - Sets the separator between the units (defaults to ".").
- `delimiter` - Sets the thousands delimiter (defaults to ",").
- `format` - Sets the format for non-negative numbers (defaults to "%u%n").
Fields are `%u` for the currency, and `%n` for the number.
- `negativeFormat` - Sets the format for negative numbers (defaults to
prepending a hyphen to the formatted number given by `format`). Accepts the
same fields than `format`, except `%n` is here the absolute value of the
number.
- `stripInsignificantZeros` - If `true` removes insignificant zeros after the
decimal separator (defaults to `false`).
- `raise` - If `true`, raises exception for non-numeric values like NaN and
Infinite values.

##### Examples

```js
i18n.numberToCurrency(1234567890.5);
// => "$1,234,567,890.50"

i18n.numberToCurrency(1234567890.506);
// => "$1,234,567,890.51"

i18n.numberToCurrency(1234567890.506, { precision: 3 });
// => "$1,234,567,890.506"

i18n.numberToCurrency("123a456");
// => "$123a456"

i18n.numberToCurrency("123a456", { raise: true });
// => raises exception ("123a456" is not a valid numeric value)

i18n.numberToCurrency(-0.456789, { precision: 0 });
// => "$0"

i18n.numberToCurrency(-1234567890.5, { negativeFormat: "(%u%n)" });
// => "($1,234,567,890.50)"

i18n.numberToCurrency(1234567890.5, {
unit: "£",
separator: ",",
delimiter: "",
});
// => "£1234567890,50"

i18n.numberToCurrency(1234567890.5, {
unit: "£",
separator: ",",
delimiter: "",
format: "%n %u",
});
// => "1234567890,50 £"

i18n.numberToCurrency(1234567890.5, { stripInsignificantZeros: true });
// => "$1,234,567,890.5"

i18n.numberToCurrency(1234567890.5, { precision: 0, roundMode: "up" });
// => "$1,234,567,891"
```

#### `I18n#numberToPercentage`

Formats a `number` as a percentage string (e.g., 65%). You can customize the
format in the `options` hash.

##### Options

- `precision` - Sets the level of precision (defaults to 3).
- `roundMode` - Determine how rounding is performed (defaults to `default`.)
- `separator` - Sets the separator between the units (defaults to ".").
- `delimiter` - Sets the thousands delimiter (defaults to "").
- `format` - Sets the format for non-negative numbers (defaults to "%n%"). The
number field is represented by `%n`.
- `negativeFormat` - Sets the format for negative numbers (defaults to
prepending a hyphen to the formatted number given by `format`). Accepts the
same fields than `format`, except `%n` is here the absolute value of the
number.
- `stripInsignificantZeros` - If `true` removes insignificant zeros after the
decimal separator (defaults to `false`).

##### Examples

```js
i18n.numberToPercentage(100);
// => "100.000%"

i18n.numberToPercentage("98");
// => "98.000%"

i18n.numberToPercentage(100, { precision: 0 });
// => "100%"

i18n.numberToPercentage(1000, { delimiter: ".", separator: "," });
// => "1.000,000%"

i18n.numberToPercentage(302.24398923423, { precision: 5 });
// => "302.24399%"

i18n.numberToPercentage(1000, { precision: null });
// => "1000%"

i18n.numberToPercentage("98a");
// => "98a%"

i18n.numberToPercentage(100, { format: "%n %" });
// => "100.000 %"

i18n.numberToPercentage(302.24398923423, { precision: 5, roundMode: "down" });
// => "302.24398%"
```

#### `I18n#numberToDelimited`

Formats a `number` with grouped thousands using `delimiter` (e.g., 12,324). You
can customize the format in the `options` object.

##### Options

- `delimiter` - Sets the thousands delimiter (defaults to ",").
- `separator` - Sets the separator between the fractional and integer digits
(defaults to ".").
- `delimiterPattern` - Sets a custom regular expression used for deriving the
placement of delimiter. Helpful when using currency formats like INR. The
regular expression must be global (i.e. it has the `g` flag).

##### Examples

```js
i18n.numberToDelimited(12345678);
// => "12,345,678"

i18n.numberToDelimited("123456");
// => "123,456"

i18n.numberToDelimited(12345678.05);
// => "12,345,678.05"

i18n.numberToDelimited(12345678, { delimiter: "." });
// => "12.345.678"

i18n.numberToDelimited(12345678, { delimiter: "," });
// => "12,345,678"

i18n.numberToDelimited(12345678.05, { separator: " " });
// => "12,345,678 05"

i18n.numberToDelimited("112a");
// => "112a"

i18n.numberToDelimited(98765432.98, { delimiter: " ", separator: "," });
// => "98 765 432,98"

i18n.numberToDelimited("123456.78", {
delimiterPattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/g,
});
// => "1,23,456.78"
```

#### `I18n#numberToRounded`

Formats a `number` with the specified level of `precision` (e.g., 112.32 has a
precision of 2 if `significant` is `false`, and 5 if `significant` is `true`).
You can customize the format in the `options` object.

##### Options

- `locale` - Sets the locale to be used for formatting (defaults to current
locale).
- `precision` - Sets the precision of the number (defaults to 3). Keeps the
number's precision if `null`.
- `RoundMode` - Determine how rounding is performed (defaults to :default).
- `significant` - If `true`, precision will be the number of significant_digits.
If `false`, the number of fractional digits (defaults to `false`).
- `separator` - Sets the separator between the fractional and integer digits
(defaults to ".").
- `delimiter` - Sets the thousands delimiter (defaults to "").
- `stripInsignificantZeros` - If `true` removes insignificant zeros after the
decimal separator (defaults to `false`).

##### Examples

```js
i18n.numberToRounded(111.2345);
// => "111.235"

i18n.numberToRounded(111.2345, { precision: 2 });
// => "111.23"

i18n.numberToRounded(13, { precision: 5 });
// => "13.00000"

i18n.numberToRounded(389.32314, { precision: 0 });
// => "389"

i18n.numberToRounded(111.2345, { significant: true });
// => "111"

i18n.numberToRounded(111.2345, { precision: 1, significant: true });
// => "100"

i18n.numberToRounded(13, { precision: 5, significant: true });
// => "13.000"

i18n.numberToRounded(13, { precision: null });
// => "13"

i18n.numberToRounded(389.32314, { precision: 0, roundMode: "up" });
// => "390"

i18n.numberToRounded(13, {
precision: 5,
significant: true,
stripInsignificantZeros: true,
});
// => "13"

i18n.numberToRounded(389.32314, { precision: 4, significant: true });
// => "389.3"

i18n.numberToRounded(1111.2345, {
precision: 2,
separator: ",",
delimiter: ".",
});
// => "1.111,23"
```

#### `I18n#numberToHumanSize`

Formats the bytes in `number` into a more understandable representation (e.g.,
giving it 1500 yields 1.46 KB). This method is useful for reporting file sizes
to users. You can customize the format in the `options` object.

See `I18n#numberToHuman` if you want to pretty-print a generic number.

##### Options

- `precision` - Sets the precision of the number (defaults to 3).
- `roundMode` - Determine how rounding is performed (defaults to `default`)
- `significant` - If `true`, precision will be the number of significant_digits.
If `false`, the number of fractional digits (defaults to `true`)
- `separator` - Sets the separator between the fractional and integer digits
(defaults to ".").
- `delimiter` - Sets the thousands delimiter (defaults to "").
- `stripInsignificantZeros` - If `true` removes insignificant zeros after the
decimal separator (defaults to `true`)

##### Examples

```js
i18n.numberToHumanSize(123)
// => "123 Bytes"

i18n.numberToHumanSize(1234)
// => "1.21 KB"

i18n.numberToHumanSize(12345)
// => "12.1 KB"

i18n.numberToHumanSize(1234567)
// => "1.18 MB"

i18n.numberToHumanSize(1234567890)
// => "1.15 GB"

i18n.numberToHumanSize(1234567890123)
// => "1.12 TB"

i18n.numberToHumanSize(1234567890123456)
// => "1.1 PB"

i18n.numberToHumanSize(1234567890123456789)
// => "1.07 EB"

i18n.numberToHumanSize(1234567, {precision: 2})
// => "1.2 MB"

i18n.numberToHumanSize(483989, precision: 2)
// => "470 KB"

i18n.numberToHumanSize(483989, {precision: 2, roundMode: "up"})
// => "480 KB"

i18n.numberToHumanSize(1234567, {precision: 2, separator: ","})
// => "1,2 MB"

i18n.numberToHumanSize(1234567890123, {precision: 5})
// => "1.1228 TB"

i18n.numberToHumanSize(524288000, {precision: 5})
// => "500 MB"
```

#### `I18n#numberToHuman`

Pretty prints (formats and approximates) a number in a way it is more readable
by humans (e.g.: 1200000000 becomes "1.2 Billion"). This is useful for numbers
that can get very large (and too hard to read).

See `I18n#numberToHumanSize` if you want to print a file size.

You can also define your own unit-quantifier names if you want to use other
decimal units (e.g.: 1500 becomes "1.5 kilometers", 0.150 becomes "150
milliliters", etc). You may define a wide range of unit quantifiers, even
fractional ones (centi, deci, mili, etc).

##### Options

- `precision` - Sets the precision of the number (defaults to 3).
- `roundMode` - Determine how rounding is performed (defaults to `default`).
- `significant` - If `true`, precision will be the number of significant_digits.
If `false`, the number of fractional digits (defaults to `true`)
- `separator` - Sets the separator between the fractional and integer digits
(defaults to ".").
- `delimiter` - Sets the thousands delimiter (defaults to "").
- `stripInsignificantZeros` - If `true` removes insignificant zeros after the
decimal separator (defaults to `true`)
- `units` - A Hash of unit quantifier names. Or a string containing an i18n
scope where to find this hash. It might have the following keys:
- _integers_: `unit`, `ten`, `hundred`, `thousand`, `million`, `billion`,
`trillion`, `quadrillion`
- _fractionals_: `deci`, `centi`, `mili`, `micro`, `nano`, `pico`, `femto`
- `format` - Sets the format of the output string (defaults to "%n %u"). The
field types are:
- %u - The quantifier (ex.: 'thousand')
- %n - The number

##### Examples

```js
i18n.numberToHuman(123);
// => "123"

i18n.numberToHuman(1234);
// => "1.23 Thousand"

i18n.numberToHuman(12345);
// => "12.3 Thousand"

i18n.numberToHuman(1234567);
// => "1.23 Million"

i18n.numberToHuman(1234567890);
// => "1.23 Billion"

i18n.numberToHuman(1234567890123);
// => "1.23 Trillion"

i18n.numberToHuman(1234567890123456);
// => "1.23 Quadrillion"

i18n.numberToHuman(1234567890123456789);
// => "1230 Quadrillion"

i18n.numberToHuman(489939, { precision: 2 });
// => "490 Thousand"

i18n.numberToHuman(489939, { precision: 4 });
// => "489.9 Thousand"

i18n.numberToHuman(489939, { precision: 2, roundMode: "down" });
// => "480 Thousand"

i18n.numberToHuman(1234567, { precision: 4, significant: false });
// => "1.2346 Million"

i18n.numberToHuman(1234567, {
precision: 1,
separator: ",",
significant: false,
});
// => "1,2 Million"

i18n.numberToHuman(500000000, { precision: 5 });
// => "500 Million"

i18n.numberToHuman(12345012345, { significant: false });
// => "12.345 Billion"
```

Non-significant zeros after the decimal separator are stripped out by default
(set `stripInsignificantZeros` to `false` to change that):

```js
i18n.numberToHuman(12.00001);
// => "12"

i18n.numberToHuman(12.00001, { stripInsignificantZeros: false });
// => "12.0"
```

You can also use your own custom unit quantifiers:

```js
i18n.numberToHuman(500000, units: { unit: "ml", thousand: "lt" })
// => "500 lt"
```

If in your I18n locale you have:

```yaml
---
en:
distance:
centi:
one: "centimeter"
other: "centimeters"
unit:
one: "meter"
other: "meters"
thousand:
one: "kilometer"
other: "kilometers"
billion: "gazillion-distance"
```

Then you could do:

```js
i18n.numberToHuman(543934, { units: "distance" });
// => "544 kilometers"

i18n.numberToHuman(54393498, { units: "distance" });
// => "54400 kilometers"

i18n.numberToHuman(54393498000, { units: "distance" });
// => "54.4 gazillion-distance"

i18n.numberToHuman(343, { units: "distance", precision: 1 });
// => "300 meters"

i18n.numberToHuman(1, { units: "distance" });
// => "1 meter"

i18n.numberToHuman(0.34, { units: "distance" });
// => "34 centimeters"
```

### Date Formatting

The `I18n#localize` (or its alias `I18n#l`) can accept a string, epoch time
integer or a `Date` object. You can see below the accepted formats:

```js
// yyyy-mm-dd
i18n.l("date.formats.short", "2009-09-18");

// yyyy-mm-dd hh:mm:ss
i18n.l("time.formats.short", "2009-09-18 23:12:43");

// JSON format with local Timezone (part of ISO-8601)
i18n.l("time.formats.short", "2009-11-09T18:10:34");

// JSON format in UTC (part of ISO-8601)
i18n.l("time.formats.short", "2009-11-09T18:10:34Z");

// Epoch time
i18n.l("date.formats.short", 1251862029000);

// mm/dd/yyyy
i18n.l("date.formats.short", "09/18/2009");

// Date object
i18n.l("date.formats.short", new Date());
```

You can also add placeholders to the date format:

```js
const i18n = new I18n({
date: {
formats: {
ordinalDay: "%B %{day}",
},
},
});

i18n.l("date.formats.ordinalDay", "2009-09-18", { day: "18th" }); // Sep 18th
```

If you prefer, you can use the `I18n#toTime` and `I18n#strftime` functions
directly to format dates.

```js
var date = new Date();
i18n.toTime("date.formats.short", "2009-09-18");
i18n.toTime("date.formats.short", date);
i18n.strftime(date, "%d/%m/%Y");
```

The accepted formats for `i18n.strftime` are:

```
%a - The abbreviated weekday name (Sun)
%A - The full weekday name (Sunday)
%b - The abbreviated month name (Jan)
%B - The full month name (January)
%c - The preferred local date and time representation
%d - Day of the month (01..31)
%-d - Day of the month (1..31)
%H - Hour of the day, 24-hour clock (00..23)
%-H - Hour of the day, 24-hour clock (0..23)
%k - Hour of the day, 24-hour clock (0..23)
%I - Hour of the day, 12-hour clock (01..12)
%-I - Hour of the day, 12-hour clock (1..12)
%l - Hour of the day, 12-hour clock (1..12)
%m - Month of the year (01..12)
%-m - Month of the year (1..12)
%M - Minute of the hour (00..59)
%-M - Minute of the hour (0..59)
%p - Meridian indicator (AM or PM)
%P - Meridian indicator (am or pm)
%S - Second of the minute (00..60)
%-S - Second of the minute (0..60)
%w - Day of the week (Sunday is 0, 0..6)
%y - Year without a century (00..99)
%-y - Year without a century (0..99)
%Y - Year with century
%z - Timezone offset (+0545)
%Z - Timezone offset (+0545)
```

Check out
[\_\_tests\_\_/strftime.test.ts](https://github.com/fnando/i18n/blob/main/__tests__/strftime.test.ts)
file for more examples!

Finally, you can also diplay relative time strings using `I18n#timeAgoInWords`.

```js
const to = new Date();
const from = to.getTime() - 60 * 60 * 1000; // ~1h ago.

i18n.timeAgoInWords(from, to);
//=> about 1 hour
```

#### Using pluralization and number formatting together

Sometimes you might want to display translation with formatted number, like
adding thousand delimiters to displayed number You can do this:

```js
const i18n = new I18n({
en: {
points: {
one: "1 Point",
other: "{{points}} Points",
},
},
});

const points = 1234;

i18n.t("points", {
count: points,
points: i18n.formatNumber(points),
});
```

Output should be `1,234 points`.

### Other helpers

#### `I18n#toSentence(list, options)`

```js
i18n.toSentence(["apple", "banana", "pineapple"]);
//=> apple, banana, and pineapple.
```

## Troubleshooting

### I'm getting an error like `Unable to resolve "make-plural" from "node modules/i18n-js/dist/import/Pluralization.js"`

[make-plural](https://www.npmjs.com/package/make-plural) uses `.mjs` files. You
need to change your build pipeline to also consider these files.

If you're using [react-native](https://reactnative.dev), you need to change your
metro config to consider `.mjs`. Try doing something like this (you may need to
adapt your code based on existing changes).

```js
const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
const {
resolver: { assetExts, sourceExts },
} = await getDefaultConfig();

return {
resolver: {
sourceExts: [...sourceExts, "mjs"],
},
};
})();
```

### I'm getting an error like `SyntaxError: Unexpected end of JSON input` or `Uncaught SyntaxError: Unexpected token ;`

You may get such error if you're trying to load empty JSON files with
`import data from "file.json"`. This has nothing to do with I18n and is related
to how your JSON file is loaded. **JSON files must contain valid JSON data.**

Similarly, make sure you're writing valid JSON, and not JavaScript. For
instance, if you write something like `{};`, you'll get an error like
`Uncaught SyntaxError: Unexpected token ;`.

### My JSON contains a flat structure. How can I load and use it with I18n.js?

I18n.js expects a nested object to represent the translation tree. For this
reason, you cannot use an object like the following by default:

```json
{
"en.messages.hello": "hello",
"pt-BR.messages.hello": "olá"
}
```

One solution is using something like the following to transform your flat into a
nested object:

```js
const { set } = require("lodash");

const from = {
"en.messages.hello": "hello",
"pt-BR.messages.hello": "olá",
};

function flatToNestedObject(target) {
const nested = {};

Object.keys(target).forEach((path) => set(nested, path, target[path]));

return nested;
}

console.log(flatToNestedObject(from));
// {
// en: { messages: { hello: 'hello' } },
// 'pt-BR': { messages: { hello: 'olá' } }
// }
```

You can also use something like [flat](https://github.com/hughsk/flat) to
perform the same transformation.

## Maintainer

- [Nando Vieira](https://github.com/fnando)

## Contributors

- https://github.com/fnando/i18n/contributors

## Contributing

For more details about how to contribute, please read
https://github.com/fnando/i18n/blob/main/CONTRIBUTING.md.

## License

The gem is available as open source under the terms of the
[MIT License](https://opensource.org/licenses/MIT). A copy of the license can be
found at https://github.com/fnando/i18n/blob/main/LICENSE.md.

## Code of Conduct

Everyone interacting in the i18n project's codebases, issue trackers, chat rooms
and mailing lists is expected to follow the
[code of conduct](https://github.com/fnando/i18n/blob/main/CODE_OF_CONDUCT.md).