A super-lightweight and versatile localisation library. Available via https://www.npmjs.com/package/@lxg/l10n
With this library, you can build multilingual frontend application with very little overhead. It weights less than 1kB minified and compressed, and it has lots of nice features. At the same time, it comes with powerful build-time features, such as the translation extractor that will find all translatable messages in your code and add them to your translations file. No need to manually extract and merge your translations!
Under the hood, the library uses the Gettext .po files as an intermediate format which you or your translators can edit. From those human-readable PO files, the actual size-optimised dictionaries are generated.
.po
catalog format is widely supported, so you will always find translators, tools and services which can work with your catalog files.See the @lxg/l10n-demo
for an example of how this works in practice. You can also use that demo as a blueprint for your own project.
For the impatient, or if you already have experience with the library, here are the main steps in a nutshell.
npm i @lxg/l10n
.l10n
config to your package.json
and set the correct values (see below).npx l10n -ec
to create the PO message catalogs and the translation tables.translations.json
to your code.L10n
class and start wrapping your messages in the translation functions.npx l10n -ec
as often as needed in order to update message catalogs and translation tables.@lxg/l10n/html
module and with the <l10n:t>
, <l10n:n>
and <l10n:x>
tags in your code.In case you don’t know what these steps mean in detail, please read the detailled documentation below.
This package is available via NPM:
npm i @lxg/l10n
First, import the library into your application:
import L10n from "@lxg/l10n"
Now you can create an instance of your translator:
const l10n = new L10n({}, "de-DE")
(Ignore the {}
argument for now, we will replace it with something useful later.)
To make your code translation-ready, you can simply wrap all UI strings into one of the three translation functions l10n.t
, l10n.x
and l10n.n
(see “Translation Functions” below).
For example, the following usage of l10n.t
tells the library that Hello World!
is a string to be translated.
console.log(l10n.t("Hello World!"))
The following also works:
console.log(`=== ${l10n.t("Template strings are awesome!")} ===`)
Or how about JSX/TSX?
<span>{l10n.t("Also works with JSX!")}</span>
There are three translation functions you can use in your code:
l10n.t(message)
: This function simply looks for a translated string in the translation table. If it finds one, it returns the translation; otherwise it returns the original string.
l10n.x(context, message)
: This function scopes the message to a context. This is useful when two original strings have different meanings and will most likely require different translations.
For example, in a financial context, the English word “amount” would translate to the German „Betrag“, while in a context where items are counted, the German translation would be „Anzahl“. Prefixing the translation with a context allows translators to add both translations.
l10n.n(singular_message, plural_message, count)
: The n() function is the pluralisation function. It takes three parameters: The English singular, the English plural, and the actual number it refers to. The function uses “pluralisation rules” to determine the correct form in the target language. For example, many languages have more than two plural forms, and this function will select the correct one.
NOTE: This function will not fill in the value into the placeholder in the plural form, but only return the correct message. See below for a simple sprintf() implementation to do the substitution for you.
sprintf()
implementationTo fill the placeholders in translations, especially pluralisations, you can use something like sprintf()
. This is not part of the l10n library, but you can use the following super-simple sprintf()
implementation:
const sprintf = (format, ...args) => {
let i = 0;
return format.replace(/%s/g, () => args[i++]);
}
Use it like this:
// Simple placeholder for a name
sprintf(l10n.t("Hello %s!"), "John Doe")
// Pluralisation. Note that you must pass the number twice: Once to n() and
// once to sprintf()
sprintf(l10n.n("One apple", "%s apples", 4), 4)
This is of course just a very basic example. You may want to use a more sophisticated implementation of sprintf
(e.g. with support for positional parameters) in your project.
The catalog manager is a CLI tool and has two tasks:
.po
catalog.The npx l10n
tool is the CLI frontend to the catalog manager.
Before you can start using the catalog manager, you must add some configuration to your project’s package.json
file:
{
"@lxg/l10n": {
"directory": "l10n",
"instance": "l10n",
"locales": [
"de-DE",
"fr-FR"
],
"sources": [
"main.js",
"other.js",
"src/*"
],
"targets": {
"l10n/translations.json": [
"first.js",
"src/*"
]
}
}
}
The directory
key specifies where the translations catalogs will be stored. It is optional and defaults to l10n
.
The instance
key specifies the variable name of your instance of the L10n
class. It is optional and defaults to l10n
. It can also reflect a deeper structure suche as this.l10n
, some.object.l10n
or even multiple values like ["this.l10n", "some.object.l10n"]
.
The locales
key specifies the locales into which your package should be translated. The format for locales is: two lowercase letters for the language, followed by a hyphen (not an underscore!), followed by two uppercase letters for the region/country. NOTE: This tool assumes the en-GB
locale as default, therefore you don’t need to add it.
The sources
key contains a list which specifies the files to be considered for the catalog. Each item in this list can either be a verbatim file name or a glob expression. If you’re using this library for HTML localisation as well, you must use the extended syntax, where source
is a map of parsers to file globs. (See the chapter on HTML localisation below)
The targets
key is an object, where each entry is a target file for the JSON dictionary mapped to a list of sources. Each item in this list can either be a verbatim file name or a glob expression. By mapping the output target to a subset of the source files, we can build multiple translation dictionaries for different parts of your application, allowing smaller downloads e.g. in lazy-loading setups.
You can now call npx l10n --extract
to extract translatable messages in the l10n.t
, l10n.n
, l10n.x
functions from your source files. This will generate one or more .po
files in the location you provided as directory
in your config. Note: The extractor will remove obsolete translations from the dictionary in order to avoid stuffing unneeded translations in the output table.
After translating the strings in the .po
files, you can generate the output table(s) with npx l10n --compile
. This will create the JSON tables at the locations specified in your config. Important: Do NOT modify the JSON files directly! They will be overwritten next time you run the npx l10n --compile
command.
You can also combine the extract and compile commands by running npx l10n --extract --compile
, or use the shorthand npx l10n -ec
.
PLEASE NOTE: The extraction tool cannot parse TSX/JSX or Typescript files. In these cases, you should first transpile the native JS code, and then reference the generated output as sources in your package.json
.
Assuming you are using the configuration from the above example, the extractor will create or update the catalogs for German and French. Catalogs would be stored in the ./l10n
directory. So after running the command for the first time, you will find the new files ./l10n/de-DE.po
and ./l10n/fr-FR.po
in your project.
The *.po
files can be given to a human translator or be run through a translation tool which supports this format (there are lots of them). After the .po
files have been updated, you can run npx l10n
again, to create the translation dictionaries.
All .po files and JSON dictionaries should be put under version control.
This tool uses a JavaScript parser (Acorn) to find translatable strings in the source files. If you want to process TypeScript, TSX, JSX or other formats which cannot be processed by the parser, you must first generate the native JS code from them, and then reference the generated output as sources.
After the PO files have been translated and the translation tables have been generated, you can use them in your code base.
At this point you should have generated your JSON translation tables with the catalog manager. Assuming you have specified ./l10n/translations.json
as the translation file, you can now load it in your applcation and pass it to the translator.
Loading the JSON can be a bit tricky, depending on the tools you have at hand. The easiest way is to use a bundler like Parcel or Rollup. It lets you import the JSON file synchronously, just like a JS file:
// as an ESmodule, may require a plugin for your bundler (e.g. @rollup/plugin-json)
import translations from "./l10n/translations.json"
Now you can pass the translation table to the L10n
constructor as the first parameter:
const l10n = new L10n(translations, "de-DE")
NOTE: If other modules/classes also need translations, you can either pass the l10n
instance to them, or load the library and the JSON translation table there again. Keep in mind that the latter might increase the size of your build artifact.
The library can also be used to translate strings in HTML, which works well with Static Site Generators. If you happen to use the Eleventy SSG, there’s even a handy plugin available.
First, you must register the files that should be considered by the HTML parser. This is done by using the extended format on the source
field on our package.json
configuration. Assuming you have your JavaScript files in one folder and the HTML files in another one, it might look like this:
{
"@lxg/l10n": {
"sources": {
"html": [
"src/html/**/*"
],
"js": [
"src/js/**/*"
]
}
}
}
Now you can start adding multilingual strings in your template code. The following examples show you how:
<!-- Simple translations, equivalent to the L10n.t() function -->
<l10n:t>It works!</l10n:t>
<!-- Use placeholders by passing them as attributes, and then referencing them wrapped in % signs -->
<l10n:t year=2022>© ACME Ltd. %year%. All rights reserved.</l10n:t>
<!-- Plural-aware translations, equivalent to the L10n.n() function -->
<!-- NOTE: You must use the #num attribute to allow us to pick the correct variant -->
<l10n:n #num=4>
<l10n:singular>1 child</l10n:singular>
<l10n:plural>%#num% children</l10n:plural>
</l10n:n>
<!-- Context-aware translations, equivalent to the L10n.x() function -->
<!-- NOTE: You must use the #context attribute to indicate the context -->
<l10n:x #context=monetary>amount</l10n:x>
<l10n:x #context=count>amount</l10n:x>
To extract messages to the catalog and to compile the translations table, you can use npx l10n -ec
just as usual.
Now you can use the @lxg/l10n/html
module to generate translated output. See the following example:
const l10nHtml = require("@lxg/l10n/html")
const translations = require("./translations.json")
const lang = "de-DE"
const source = "<p><l10n:t>Hello World!</l10n:t></p>"
const translated = l10nHtml(source, translations, lang)
console.log(translated)
date.js
ToolIf you’re working with dates, you will face two additional problems:
Y-m-d
in English and d.m.Y
in German),This is solved by the date.js
module.
Consider the following example, especially how the functions are arranged to produce the desired output:
const L10n = require('@lxg/l10n');
const { L10nDateFormat } = require("@lxg/l10n/date")
const translations = require('./l10n/translations.json');
const l10n = new L10n(translations, "de-DE")
const l10nDateFormat = new L10nDateFormat(l10n)
// English: Today is April 23, 2021.
// German: Heute ist der 23. April 2021.
sprintf(l10n.t("Today is %s."), l10nDateFormat.fmt(new Date(2021, 3, 23), l10n.t("F j, Y")))
The date.js
module also provides the L10nDate
class to get translated month/weekday names directly:
const L10n = require('@lxg/l10n');
const L10nDate = require("@lxg/l10n/date")
const translations = require('./l10n/translations.json');
const l10n = new L10n(translations, "de-DE")
const l10nDate = new L10nDate(l10n)
l10nDate.getMonths() // Returns a list of month names in the current locale: “January”, “February”, …
l10nDate.getMonthsShort() // Returns a list of short month names in the current locale: “Jan”, “Feb”, …
l10nDate.getWeekdays() // Returns a list of weekday names in the current locale: “Monday”, “Tuesday”, …
l10nDate.getWeekdaysShort() // Returns a list of short weekday names in the current locale: “Mon”, “Tue”, …
l10nDate.getFirstDayOfWeek() // Returns the first day of the week, 0: Sunday, 1: Monday, 5: Friday, 6: Saturday
l10nDate.shiftWeekdays() // To be used together with one of the weekdays functions to get the localized order of days, e.g. `l10nDate.shiftWeekdays(l10nDate.getMonthsShort())
Translations of localised weekday/month names will be automatically added to your translations table, based on the locales you have configured. This is achieved by adding the l10n:date:*
pseudo-paths to the config in your package.json file:
{
"@lxg/l10n": {
"targets": {
"l10n/translations.json": [
"src/*",
"l10n:date:firstday",
"l10n:date:months",
"l10n:date:monthsShort",
"l10n:date:days",
"l10n:date:daysShort"
]
}
}
}
Make sure to only use the ones you need, because they add some weight, especially if you have multiple target languages.