Skip to content
/ yaintl Public

Yet another i18n library with ICU message syntax

License

Notifications You must be signed in to change notification settings

tuplo/yaintl

Repository files navigation


Logo

yaintl

Yet another i18n library with ICU message syntax (tiny footprint)

Why

Other i18n solutions supporting ICU syntax were too heavy to bundle with our apps where every kb in size counts. So we wrote this stripped down to the basics solution while still supporting complex ICU syntax and a familiar API.

Install

$ npm install @tuplo/yaintl

# or with yarn
$ yarn add @tuplo/yaintl

Usage

import I18n from '@tuplo/yaintl';

const i18n = new I18n({ 
  locale: "en-GB", 
  messages: { simple: { message: "Hi {name}!" } }
})

const t = i18n.build('simple')

t('message', { name: "Alice" }) // ⇒ "Hi Alice!"

With React

Using React.Context, we create a top-level component with a Provider holding all the i18n messages for the chosen locale. Then, on each component that needs it, we use a custom hook returning the i18n lookup/formatting function.

import { createContext, useContext } from "react";
import I18n from "@tuplo/yaintl"

const I18nContext = createContext({});

const messages = {
  "en-GB": { "greeting": "Fancy a cuppa, {name}?" },
  "en-US": { "greeting": "Howdy, {name}!" },
  "fr-FR": { "greeting": "Comment ça va, {name}?" },
}

function useI18n(prefix?: string) {
  return useContext(I18nContext).build(prefix);
}

function Greeting() {
  const t = useI18n();

  return <h1>{t("greeting", { name: "Oliver" })}</h1>;
}

function App() {
  const [locale, setLocale] = useState("en-GB");
  const engine = new I18n({ locale, messages: messages[locale] });

  const onChangeLocale = (event: ChangeEvent<HTMLSelectElement>) => {
    setLocale(event.currentTarget.value);
  };

  return (
    <I18nContext.Provider value={engine}>
      <select onChange={onChangeLocale}>
        <option value="en-GB">English (UK)</option>
        <option value="en-US">English (US)</option>
        <option value="fr-FR">French</option>
      </select>
      <Greeting name="Oliver" />
    </I18nContext.Provider>
  );
}

const domNode = document.getElementById("root");
const root = createRoot(domNode as HTMLElement);
root.render(<App />);

// ⇒ "Fancy a cuppa, Oliver?" (en-GB)

See the example on: examples/react.

npm run run:examples:react

Message syntax

We use the ICU syntax to declare i18n messages. Here's a brief description of how to use this syntax.

Plain string

const messages = {
  hello: "Hi stranger!"
}

t('hello'); // ⇒ "Hi stranger!"

Interpolation

Where placeholders in the message are replaced by given values.

Placeholder

const messages = {
  desc: "{name} lives in {city}."
}

t('desc', { name: "Alice", city: "London" } ); // ⇒ "Alice lives in London."

Plural

const messages = {
  photos: "You have {count, plural, one {# photo} other {# photos}}."
}

t('photos', { count: 1 }) // ⇒ "You have 1 photo."
t('photos', { count: 12 }) // ⇒ "You have 12 photos."

Plural with offset

const messages = {
  adds: "{adds, plural, offset:1 =0 {No-one has added this} =1 {You added this} one {You and one other person added this} other {You and # others added this}}."
}

t('adds', { adds: 0 }); // ⇒ "No-one has added this."
t('adds', { adds: 1 }); // ⇒ "You added this."
t('adds', { adds: 2 }); // ⇒ "You and one other person added this."
t('adds', { adds: 12 }); // ⇒ "You and 11 others added this."

Ordinal

const messages = {
  queue: "You are the {pos, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}.",
}

t('queue', { pos: 1 }); // ⇒ "You are the 1st."
t('queue', { pos: 2 }); // ⇒ "You are the 2nd."
t('queue', { pos: 3 }); // ⇒ "You are the 3rd."
t('queue', { pos: 12 }); // ⇒ "You are the 12th."

Select

const messages = {
  liked: "{gender, select, male {He} female {She} other {They}} liked this."
}

t('liked', { gender: 'male' }); // ⇒ "He liked this."
t('liked', { gender: 'female' }); // ⇒ "She liked this."
t('liked', { gender: undefined }); // ⇒ "They liked this."

Formatting

Values can also be formatted based on their type by using the syntax {variable, type, format}. Example: "The default value is {count, number, decimal}."

  • variable is the variable we pass
  • type is how to interpret the value
  • format is optional, and is a further refinement on how to display that type of data

Number

Possible values from style option on Intl.NumberFormat.

const messages = {
  num: "The default value is {count, number}.",
  perc: "The tank is at {count, number, percent} capacity.",
}

t('num', { count: 1_499 }); // ⇒ "The default value is 1,499."
t('perc', { count: 0.76 }); // ⇒ "The tank is at 76% capacity."

Date

Possible values from dateStyle option on Intl.DateTimeFormat.

const messages = {
  dt1: "Sale begins { startDate, date, short }.",
  dt2: "Sale begins { startDate, date, medium }.",
  dt3: "Sale begins { startDate, date, long }.",
  dt4: "Sale begins { startDate, date, full }.",
}

const startDate = new Date('2022-12-25');
t('dt1', { startDate }); // ⇒ "Sale begins 25/12/2022."
t('dt2', { startDate }); // ⇒ "Sale begins 25 Dec 2022."
t('dt3', { startDate }); // ⇒ "Sale begins 25 December 2022."
t('dt4', { startDate }); // ⇒ "Sale begins Sunday, 25 December 2022."

Time

Possible values from timeStyle option on Intl.DateTimeFormat.

const messages = {
  tm1: "Coupon expires at { startTime, time, short }.",
  tm2: "Coupon expires at { startTime, time, medium }.",
  tm3: "Coupon expires at { startTime, time, long }.",
  tm4: "Coupon expires at { startTime, time, full }.",
}

const startTime = new Date('2022-12-25T12:34:00.000Z');
t('tm1', { startTime }); // ⇒ "Coupon expires at 12:34."
t('tm2', { startTime }); // ⇒ "Coupon expires at 12:34:00."
t('tm3', { startTime }); // ⇒ "Coupon expires at 12:34:00 GMT."
t('tm4', { startTime }); // ⇒ "Coupon expires at 12:34:00 GMT."

List

Possible values from style option on Intl.ListFormat.

const messages = {
  l1: "With { team, list }.",
  l2: "With { team, list, long }.",
  l3: "With { team, list, short }.",
  l4: "With { team, list, narrow }.",
}

const team = ['Alice', 'Bob', 'Charlie'];
t('l1', { team }); // ⇒ "With Alice, Bob and Charlie."
t('l2', { team }); // ⇒ "With Alice, Bob and Charlie."
t('l3', { team }); // ⇒ "With Alice, Bob and Charlie."
t('l4', { team }); // ⇒ "With Alice, Bob, Charlie."

Custom formatters

Besides the default styles from Intl formatters, we can use all those options to create custom formatters.

Number

All possible options listed at Intl.NumberFormat.

const formats = {
  number: {
    nf1: { notation: 'scientific' },
    nf2: { signDisplay: 'exceptZero' }  
  }
}
const messages = {
  'm1': 'The value is {count, number, nf1}.',
  'm2': 'The value is {count, number, nf2}.'
}
const i18n = new I18n({ locale: 'en-GB', messages, formats });

const count = 1_234_567_890;
t('m1', { count }) // ⇒ "The value is 1.235E9."
t('m2', { count }) // ⇒ "The value is +1,234,567,890."

Date

All possible options listed at Intl.DateTimeFormat.

const formats = {
  dateTime: {
    df1: { day: 'numeric', month: 'short' },
    df2: { month: 'long' }  
  }
}
const messages = {
  'm1': 'Sale begins {start, date, df1}.',
  'm2': 'Sale begins {start, date, df2}.'
}
const i18n = new I18n({ locale: 'en-GB', messages, formats });

const start = new Date('2022-12-25');
t('m1', { start }) // ⇒ "Sale begins 25 Dec."
t('m2', { start }) // ⇒ "Sale begins December."

Time

All possible options listed at Intl.DateTimeFormat.

const formats = {
  dateTime: {
    tf1: { timeStyle: 'short' },
    tf2: { timeStyle: 'short', timeZone: 'America/Los_Angeles' }  
  }
}
const messages = {
  'm1': 'Sale begins {start, time, tf1}.',
  'm2': 'Sale begins {start, time, tf2}.'
}
const i18n = new I18n({ locale: 'en-GB', messages, formats });

const start = new Date('2022-12-25T23:30:00.000Z');
t('m1', { start }) // ⇒ "Sale begins 23:30."
t('m2', { start }) // ⇒ "Sale begins 15:30."

List

All possible options listed at Intl.ListFormat.

const formats = {
  dateTime: {
    lf1: { type: 'disjunction' }
  }
}
const messages = {
  'm1': 'With {team, list, lf1}.'
}
const i18n = new I18n({ locale: 'en-GB', messages, formats });

const team = ['Alice', 'Bob', 'Charlie'];
t('m1', { team }) // ⇒ "With Alice, Bob or Charlie."

License

MIT