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.
$ npm install @tuplo/yaintl
# or with yarn
$ yarn add @tuplo/yaintl
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!"
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
We use the ICU syntax to declare i18n messages. Here's a brief description of how to use this syntax.
const messages = {
hello: "Hi stranger!"
}
t('hello'); // ⇒ "Hi stranger!"
Where placeholders in the message are replaced by given values.
const messages = {
desc: "{name} lives in {city}."
}
t('desc', { name: "Alice", city: "London" } ); // ⇒ "Alice lives in London."
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."
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."
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."
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."
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 passtype
is how to interpret the valueformat
is optional, and is a further refinement on how to display that type of data
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."
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."
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."
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."
Besides the default styles from Intl
formatters, we can use all those options to create custom formatters.
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."
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."
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."
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."
MIT