1.0.1 • Published 2 months ago

translator-factory v1.0.1

Weekly downloads
3
License
Unlicense
Repository
github
Last release
2 months ago

translator-factory

Generate localized translator functions for use in multilingual applications.

This module makes no assumptions about where the translation data is coming from or how it should be treated. It simply compiles, collates, and selects the translation messages. This allows your project to be more opinionated about stuff like translation data storage, pluralization rules, or if/how to implement message templates.

Getting started

Install

$ npm install translator-factory

Example

Let's assume you have already retrieved your translation data from the file system, database, etc.

The data is expected to be an object where each property is a valid locale identifier like en or en-US. The value of each property should be an object mapping message names to message text.

const data = {
	en: {
		gestures: {
			hello: "hello",
			goodbye: "goodbye",
		},
	},
	es: {
		gestures: {
			hello: "hola",
			goodbye: "adios",
		},
	},
	fr: {
		gestures: {
			hello: "bonjour",
			goodbye: "au revoir",
		},
	},
};

To turn this data into a translator factory:

const createTranslator = require("translator-factory");
const translator = createTranslator(data);

Now, passing a locale string to translator will return a function that returns formatted messages for that locale.

const spanish = translator("es");
const hello = { key: "gestures.hello" };
console.log(spanish(hello)); // outputs: "hola"

Notice that the nested property structure in the raw data is transformed into a flat, dot-separated, key when selected by the translate function.

API

Reading translation files

const translator = createTranslator(data, { ...options });

The default export of this module is a function that takes as parameters an object mapping locale identifiers to message key/values:

const data = {
	en: {
		greeting: "hello",
	},
	"en-US": {
		greeting: "howdy",
	},
	"en-GB": {
		greeting: "allo",
	},
};

It takes a second options object with the following defaults:

const options = {
	defaultLocale: "en",
	selectorPlugins: [],
	templateCompiler: (string) => () => string,
};

The defaultLocale option determines what set of translations should be used when the locale is not provided or there is no corresponding translation data. The default value for this option is en because it's my primary language, and I could not find a "correct" way to set this automatically.

The selectorPlugins option specifies an array of plugins that are applied before the message is selected from a locale's dictionary.

A plugin is a function that accepts a locale identifier. This function is called when the translator function is being generated, and allows you to do initialization for the plugin. It should return a function that accepts a message selector (e.g., { key: "whatever", ... }). If anything is returned from this function, it will be used in place of the original selector and passed to the next plugin in the chain.

A simple example looks like:

const plugin = (locale) => {
	return ({ key }) => {
		console.log(`a message '${key}' was selected for locale '${locale}'`);
	};
};

The templateCompiler option specifies a function that accepts the message strings and returns a compiled version of the string. The assumption is that the string is passed through a compilation function for a template engine.

The default template compiler is simply a function that returns the string unmodified.

See the recipes section for examples of both selectorPlugins and templateCompiler.

Getting a localized translator

const translate = translator(locale);

The parameter locale is expected to be an ISO 639-1 language identifier like en, es, or de. It can include an ISO 3166-2 region identifier, too, like en-US or zh-HK.

The return value is a translator function.

To get the american english translator:

const american = translator("en-US");

Translating a message

const translated = translate({ key, ...options });

The translation function takes as its only parameter a message object which is only required to have a key property corresponding to the message to translate.

console.log(american({ key: "greeting" })); // outputs: "howdy"

If using the templateCompiler option mentioned above, you can also pass a values property which is an object mapping placeholder names to values and is passed to the compiled template during message generation. See the recipes section for more details.

Recipes

Reading translation data from the filesystem

The way that I prefer to store translation data is in a directory where each file represents a locale. Each is a JSON file named something like en.json or zh-HK.json.

Let's say you have a directory ./translations:

$ ls translations
en.json  en-US.json  en-GB.json

You could create the data object like this:

const fs = require("fs");
const path = require("path");
const glob = require("glob");

const data = {};

for (const file of glob.sync("./translations/*.json")) {
	const locale = path.basename(file, ".json");
	data[locale] = JSON.parse(fs.readFileSync(file));
}

Now, data is suitable to be passed as the first argument to createTranslator.

Reading translation data from a database

Maybe your translation data is maintained through a web interface and it makes more sense to store the translations in a database.

You might store it in a PostgreSQL database table that looks like:

create table translations (
	locale text not null unique,
	data jsonb not null default '{}'
);

You could create the data object like this:

const postgres = require("postgres");

const sql = postgres(process.env.POSTGRES_URL);

const rows = await sql`select locale, data from translations`;

const data = {};

for (const row of rows) {
	data[row.locale] = row.data;
}

Express middleware

Assuming you are using something like express-locale to detect and set a locale for an incoming request, the following middleware function could be used to set up translations for incoming requests:

const express = require("express");
const createTranslator = require("translator-factory");

const translator = createTranslator(data);

function translation(req, res, next) {
	req.translate = translator(req.locale);
	next();
}

const app = express();

app.use(translation);

Translator function creation is cached, so subsequent requests using the same locale will return the same function, ensuring the fastest response.

Now, during any request, messages can be localized:

app.get("/whatever", (req, res) => {
	return res.json({
		message: req.translate({ key: "greeting" }),
	});
});

Pluralization

A common situation when generating translations is the need to alter the message depending on some count (cardinal) or rank (ordinal).

There are a number of ways to implement this, but you might start with a ready-made solution translator-pluralizer which is a plugin for this module. It uses the standard ECMAScript Internationalization API.

Once installed, you can use it like this:

const createTranslator = require("translator-factory");
const createTranslatorPluralizer = require("translator-pluralizer");

const data = {
	en: {
		apples: {
			one: "an apple",
			other: "a bunch of apples",
		},
	},
};

const selectorPlugins = [createTranslatorPluralizer];

const translator = createTranslator(data, { selectorPlugins });

const translate = translator();

console.log(translate({ key: "apples", count: 5 })); // outputs: "a bunch of apples"

Using a template engine

By default, messages are returned unchanged. You can add template support by using the templateCompiler option.

If you'd like a fast, simple, and capable template engine that doesn't have too many bells and whistles, you might try my template-constructor. It's a template class that allows you to define the placeholder syntax. If you want something that looks like ECMAScript template literal syntax, you could do this:

const createTranslator = require("translator-factory");
const Template = require("template-constructor");

const template = new Template({ prefix: "${", suffix: "}" });

const data = {
	en: {
		greeting: "Hello, ${name=friend}!",
	},
};

const templateCompiler = (string) => template.compile(string);

const translator = createTranslator(data, { templateCompiler });

const translate = translator();

console.log(translate({ key: "greeting" })); // outputs: "Hello, friend!"
console.log(translate({ key: "greeting", values: { name: "Jane" } })); // outputs: "Hello, Jane!"

You can use any template engine, however. Here's how you'd change the example above to use handlebars:

- const templateCompiler = (string) => template.compile(string);
+ const templateCompiler = (string) => handlebars.compile(string);

Contributing

Testing

$ npm test
1.0.2

2 months ago

1.0.1

2 months ago

1.0.0

2 months ago