Elevate your internationalization (i18n) workflow with Intlit: gettext-compatible formatting, simplified plural handling, and first-class TypeScript support.
- Simple and intuitive API
- Full TypeScript support for type safety
- Unified key for singular and plural forms (unlike gettext which often requires
separate
msgidandmsgid_plural) - Includes pluralization support (plugins can be optionally added for more features)
- Easy to integrate with existing projects
- Inline
/* ... */comments let you differentiate identical source strings without affecting the fallback text - Bundled Korean tagged template adds automatic particle selection when
localeis set toko
npm install intlitimport { Formatter } from "intlit";const formatter = new Formatter({
locale: "en",
});
const text = formatter.format("Hello World");
console.log(text); // Output: Hello WorldYou can pass values in the second argument and interpolate them inside the template. When no translation is registered, the original text is used as the fallback.
const formatter = new Formatter({ locale: "en" });
formatter.format("Hello, {name}!", { name: "Kelly" });
// → "Hello, Kelly!"Provide a message catalog to translate strings. The Formatter generics keep
the message keys type-safe.
const messages = {
"Hello, {name}!": "안녕하세요, {name}!",
};
const formatter = new Formatter<typeof messages>({
locale: "ko",
messages,
});
formatter.format("Hello, {name}!", { name: "Kelly" });
// → "안녕하세요, Kelly!"Intlit provides support for handling plural forms in different languages, making it easy to create grammatically correct translations.
First, let's see how to set up Formatter instances for English, Korean, and
Arabic, including specific message translations for Korean and Arabic. The
pluralization capability is available by default.
const formatter = new Formatter({
locale: "en",
});
// With count = 1 (singular)
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 1 }),
); // Output: You have 1 file.
// With count = 2 (plural)
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 2 }),
); // Output: You have 2 files.const messages = {
"You have {count} file{count.other:}s{/}.": "{count}개의 파일이 있습니다.",
};
const formatter = new Formatter({
locale: "ko",
messages,
});
// With count = 1
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 1 }),
); // Output: 1개의 파일이 있습니다.const messages = {
"You have {count} file{count.other:}s{/}.":
"{count.zero:}لا يوجد لديك ملفات.{.one:}لديك ملف واحد.{.two:}لديك ملفان.{.few:}لديك {_} ملفات قليلة.{.many:}لديك {_} ملفات كثيرة.{.other:}لديك {_} ملفات.{/}",
};
const formatter = new Formatter({
locale: "ar",
messages: messages,
});
// Arabic has multiple plural forms (zero, one, two, few, many, other). 🫢
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 0 }),
); // Output: لا يوجد لديك ملفات.
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 1 }),
); // Output: لديك ملف واحد.
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 2 }),
); // Output: لديك ملفان.
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 3 }),
); // Output: لديك 3 ملفات قليلة.
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 11 }),
); // Output: لديك 11 ملفات كثيرة.
console.log(
formatter.format("You have {count} file{count.other:}s{/}.", { count: 100 }),
); // Output: لديك 100 ملفات.Hooks in Intlit behave like chained methods. The default hooks include helpers
for gendered output and else fallbacks.
const formatter = new Formatter({ locale: "en" });
formatter.format(
"{user} added {photoCount.one:}a new photo{.other:}{_} new photos{/} to {gender.male:}his{.female:}her{.other:}their{/} stream.",
{ user: "Alex", photoCount: 2, gender: "female" },
);
// → "Alex added 2 new photos to her stream."Inside nested segments the current value is available through the special _
parameter, so you can reuse the original number (as seen above when printing the
plural count).
Inline block comments are stripped from the rendered output but kept in the key. They are perfect for differentiating identical phrases that need distinct translations.
const messages = {
"/* 환영 */안녕하세요.": "Welcome!",
"/* 일반 */안녕하세요.": "Hello!",
};
const formatter = new Formatter({
locale: "en",
messages,
});
formatter.format("/* 환영 */안녕하세요.");
// → "Welcome!"
formatter.format("/* 일반 */안녕하세요.");
// → "Hello!"
// Because the comments are ignored at runtime the fallback remains clean.
new Formatter({ locale: "ko" }).format("/* 환영 */안녕하세요.");
// → "안녕하세요."Need smarter whitespace management or language-specific post-processing? Intlit
ships with a particle-aware template handler that is applied automatically when
locale is set to ko.
const formatter = new Formatter({ locale: "ko" });
formatter.format(
`새 소식이 있습니다:\n{count.one:}새 메시지가 하나 생겼어요{.other:}{_}개의 새 메시지가 있어요{/}
`,
{ count: 3 },
);
// → "새 소식이 있습니다:\n3개의 새 메시지가 있어요"For other locales you can still provide your own taggedTemplate function to
massage the final string (e.g. trim indentation, collapse whitespace, or run
additional transforms).
Extend Intlit by adding application-specific hooks. A hook receives a
HookContext object describing the current placeholder and returns the next
value that should flow through the chain.
import { Formatter, type Hook } from "intlit";
const upper: Hook = (ctx) => String(ctx.current ?? "").toUpperCase();
const fallback: Hook = (ctx) =>
ctx.current == null || ctx.current === "" ? "(none)" : ctx.current;
const formatter = new Formatter({
locale: "en",
hooks: { upper, fallback },
});
formatter.format("Hello, {name.upper.fallback}!", { name: "alex" });
// → "Hello, ALEX!"Hooks receive:
ctx.original: The value that came directly fromFormatter#format.ctx.current: The value produced so far in the chain. Whatever the hook returns becomes the newctx.current.ctx.locale: Locale that was supplied when constructing the formatter.args: Arguments passed from the template method such as thecurrencypart in{price.number:currency}. Arguments can include nested templates.
To share state across hook invocations, keep data in a
WeakMap<HookContext, …>. The built-in gender and plural hooks follow this
pattern to remember which branch has already matched.
Combine custom hooks with the built-in plural helpers to cover complex cases without branching in your code.
new Formatter(options) accepts the following configuration:
locale: Locale passed to plural rules. Defaults toen.messages: A record of source text → translated text pairs.hooks: Extra hook implementations that augment or replace the defaults.taggedTemplate: Custom renderer for the template literal output.
Instantiate different Formatter objects per locale, or swap the messages
object dynamically when building multi-locale applications.