1.0.0 • Published 11 months ago

@locustjs/translation v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

locustjs-translation

This library provides multi-lingual translation of texts based on a key/value design.

Install

npm i @locustjs/translation

Import

CommonJs

var someFn = require('@locustjs/translation').someFn;

ES6

import { someFn } from '@locustjs/translation'

Types

TranslatorBase

An abstract class that defines structure of all translators. It contains translator functionality. Subclasses do not need to override this functionality in order for all translators to work the same way regarding translation. They just need to specify how they provide language resources.

Properties

PropertyDescription
currentLanggets or sets translator's current language. default is 'en'.
resourcesan object containing translation resources whose first-level properties are languages.

Methods

MethodDescription
addResource(resource, lang?, path?)adds a resource to translator.
getResource(lang)returns resources of lang language.
removeResource(lang)removes lang language resource from current translator.
translate(key: string, ...args?)translates an abstract message key based on the current language and the optional args arguments.
t(key, ...args?)a shorthand for translate() that is easier to call.

TranslatorDefault

This is a default implementation for TranslatorBase.

Basic Usage

const resources = {
    en: { ... },
    fr: { ... },
    de: { ... },
    es: { ... },
}
const translator = new TranslatorDefault();

translator.addResource(resources);

console.log(translator.translate("some.key"));

We can also use the shorter t() method.

./services/translator.js

import { TranslatorDefault } from '@locustjs/translation';

const translator = new TranslatorDefault();

translator.addResource(...);

export default translator;
import { t } from "./services/translator.js"

...
const btnOk = t(`buttons.ok`);
...

This comes in handy in UI files like Reacts JSX components.

Language Resources

Structure

Language resources are defined as a javascript object or a .json file, whose first-level property should be language name. They can be added to a translator through addResource() method.

const resources = {
    en: {
        ok: "Ok",
        cancel: "Cancel"
    },
    fa: {
        ok: "تایید",
        cancel: "انصراف"
    }
}

const translator = new TranslatorDefault();

translator.addResource(resources);

Content

The content and nesting levels of a resource object is completely arbitrary. Developers are free to choose any structure for their resources. All resources should however follow the same structure.

Example:

const resources = {
    en: {
        hello: "Hello",
        messages: {
            greeting: "Hello {0}"
        }
    },
    fr: {
        hello: "Bonjoure",
        messages: {
            greeting: "Bonjoure {0}"
        }
    },
    de: {
        hello: "Hallo",
        messages: {
            greeting: "Hallo {0}"
        }
    },
}

Language Name

We can use either full name or the shortcut 2 letter name for language in a resource. However, in reality, it can be any arbitrary name. We just need to make sure to use the same value for our translator's currentLang property.

const resourceEnUS = {
    "en-us": {
        color: "Color"
    }
}
const resourceEnUK = {
    "en-uk": {
        color: "Colour"
    }
}

const translator = new TranslatorDefault();

translator.addResource(resourceEnUS);
translator.addResource(resourceEnUK);

translator.currentLang = "en-us";

console.log(translator.translate("color"));

Content Separation

It is possible to define and add each resource separately.

const resourceEn = {
    en: {
        ok: "Ok",
        cancel: "Cancel"
    }
}
const resourceFa = {
    fa: {
        ok: "تایید",
        cancel: "انصراف"
    }
}

const translator = new TranslatorDefault();

translator.addResource(resourceEn);
translator.addResource(resourceFa);

Even resources of a single language can be defined and added as separate objects.

const resourceEnMonths = {
    en: {
        months: {
            jan: "January",
            feb: "February",
            ...
        }
    }
}
const resourceEnDays = {
    en: {
        days: {
            sun: "Sunday",
            mon: "Monday",
            ...
        }
    }
}

We just need to make sure not to use Object.assign or destructure operator upon unifying resources, since they apply shallow-merge.

const resourceEnMonths = {
    en: {
        months: {
            jan: "January",
            feb: "February",
            ...
        }
    }
}
const resourceEnDays = {
    en: {
        days: {
            sun: "Sunday",
            mon: "Monday",
            ...
        }
    }
}

let resourcesEn;

resourcesEn = Object.assign(resourceEnMonths, resourceEnDays);    // resourceEnMonths' data is lost.
resourcesEn = { ...resourceEnMonths, ...resourceEnDays };         // resourceEnMonths' data is lost again.

const translator = new TranslatorDefault();

translator.addResource(resourceEn);

In both cases (using Object.assign and destructure operator), the content of the resourceEnMonths object is lost (since its en prop is overwritten by the smae prop in resourceEnDays).

We need to perform a deep-merge in order to fix the problem. We can use the merge() function in @locustjs/extensions-object package to this aim.

...
resourcesEn = merge({}, resourceEnMonths, resourceEnDays);    // works

Fortunately, theres is no need to do this manual job. TranslatorBase performs a deep-merge in its addResource() method.

const resourceEnMonths = {
    en: {
        months: {
            jan: "January",
            feb: "February",
            ...
        }
    }
}
const resourceEnDays = {
    en: {
        days: {
            sun: "Sunday",
            mon: "Monday",
            ...
        }
    }
}

const translator = new TranslatorDefault();

translator.addResource(resourceEnMonths);   // ok
translator.addResource(resourceEnDays);     // ok

Specifying lang, path

The addResource() method, has two optional parameters named lang and path that simplifies defining resources.

Example:

const monthsEn = {
    jan: "January",
    feb: "February",
    ...
}
const daysEn = {
    sun: "Sunday",
    mon: "Monday",
    ...
}

const translator = new TranslatorDefault();

translator.addResource(monthsEn, "en", "months");
translator.addResource(daysEn, "en", "days");

As it is seen above, there is no need to define an en prop, define and a months object in it and put the months texs inside that.

The path parameter supports nesting as well.

const loginMessages = {
    success: "Welcome!",
    failed: "Login failed",
    error: "Login is not possible now."
};

translator.addResource(loginMessages, "en", "messages.account.login");

/* the end-esult will be as below:
{
    en: {
        messages: {
            account: {
                login: {
                    success: "Welcome!",
                    failed: "Login failed",
                    error: "Login is not possible now."
                }
            }
        }
    }
}
*/

json file

Resources can be put into .json files.

en.json

{
    "en": {
        "hello": "Hello",
        "messages": {
            "greeting": "Hello {0}"
        }
    }
}

fr.json

{
    "fr": {
        "hello": "Bonjoure",
        "messages": {
            "greeting": "Bonjoure {0}"
        }
    }
}

de.json

{
    "de": {
        "hello": "Hallo",
        "messages": {
            "greeting": "Hallo {0}"
        }
    }
}
import en from 'en.json';
import fr from 'fr.json';
import de from 'de.json';

const translator = new TranslatorDefault();

translator.addResource(en);
translator.addResource(fr);
translator.addResource(de);

.json files could be defined in any form, as long as they follow the same structure in order for a translator to return a value for a key.

en.json

{
    "hello": "Hello",
    "messages": {
        "greeting": "Hello {0}"
    }
}

fr.json

{
    "hello": "Bonjoure",
    "messages": {
        "greeting": "Bonjoure {0}"
    }
}

de.json

{
    "hello": "Hallo",
    "messages": {
        "greeting": "Hallo {0}"
    }
}
import en from 'en.json';
import fr from 'fr.json';
import de from 'de.json';

const translator = new TranslatorDefault({ en, fr, de })

Importing .json files resuls in adding them to the final bundle. In case our .json files are heavy, we can put them in a website and load them using TranslatorRemote. This is shown later.

Organization

It is better to have an organization for the resources, since resources could be lengthy. We are better to split each language resource into multiple parts, put parts into distinct json files and add them all to our translator, instead of having a long lengthy single json file.

For example, instead of a lengthy single .json file for a language:

{
    "numbers": {
        "one": ...,
        "two": ...,
        ...
    },
    "months": {
        "january": ...,
        "february": ...,
        ...
    },
    "days": {
        "sunday": ...,
        "monday": ...,
        ...
    }
}

We can split it into multiple files:

numbers.json

{
    "one": ...,
    "two": ...,
    ...
}

months.json

{
    "january": ...,
    "february": ...,
    ...
}

days.json

{
    "sunday": ...,
    "monday": ...,
    ...
}

We can then create an index.js file that imports the parts and returns them.

/resources
    /en
        numbers.json
        months.json
        days.json
        index.js
    /fr
        numbers.json
        months.json
        days.json
        index.js
    /de
        numbers.json
        months.json
        days.json
        index.js
    index.js

/resources/en/index.js

import numbers from './numbers.json'
import months from './months.json'
import days from './days.json'

export {
    numbers,
    months,
    days
}

Finally, the toppest index.js file imports all language resources, merges them and return the final resource.

/resources/index.js

import en from './en'
import fr from './fr'
import de from './de'

export {
    en, fr, de
}

The result can then be passed to addResource().

import { TranslatorDefault } from '@locustjs/translation';
import resources from './resources';

const translator = new TranslatorDefault();

translator.addResource(resources);

Add multiple resources

We can add multiple resources using the addResources() method.

const en = { ... }
const fr = { ... }
const de = { ... }

translator.addResources(en, fr, de);

The addResources() supports lang and path for the first two parameters as well. If it sees, the first two parameters are not object, it treats them as lang and path.

Example 1:

const en1 = {   // no need to define a 'en' prop
    months: { ... },
    days: { ... },
    seasons: { ... }
    ...
}
const en2 = {   // no need to define a 'en' prop
    messages: { ... }
    ...
}
const en3 = {   // no need to define a 'en' prop
    components: { ... }
    ...
}

translator.addResources('en', en1, en2, en3);

Example 2:

// suppose these are the messages of a service named Account that has 3 methods register(), login(), forgotPassword().

const register = {   // no need to define en: { account: { }} object
    no_username: 'Please specify a username for yourself.',
    no_password: 'Please specify a password for yourself.',
    invalid_username: 'Invalid username.'
    invalid_password: 'Password must have between {min} and {max} characters.',
    succeeded: 'Registration succeeded.',
    failed: 'Registration failed.'
}
const login = {
    no_username: 'Please enter your username.',
    no_password: 'Please enter your password.',
    succeeded: 'Welcome {name}',
    failed: 'Username/password is incorrect.'
}
const forgotPassword = {
    no_username: 'Please enter your username.',
    succeeded: 'A password reset link was sent to your email. Please check your mailbox and click the link.',
    failed: 'Sending password reset link failed. Please try again later.'
}

translator.addResources('en', 'services.account', register, login, forgotPassword);

Parametric translation

The second parameter in translate(key, ...args) method is a list of parameters that can be embeded inside translated texts.

We can define parameters for a value in a resource using {} notation.

translate() uses format() function under the hood from @locustjs/extensions-string library in order to embed the arguments. So, defining parameters and passing values follows what format() provides in this regard.

const resources = {
    en: {
        hello: "Hello",
        messages: {
            greeting: "Hello {0}"
        }
    },
    fr: {
        hello: "Bonjoure",
        messages: {
            greeting: "Bonjoure {0}"
        }
    },
    de: {
        hello: "Hallo",
        messages: {
            greeting: "Hallo {0}"
        }
    },
}

const translator = new TranslatorDefault()

translator.addResource(resources);

console.log(translator.translate('hello', 'John Doe')); // Hello John Doe

translator.currentLang = 'fr';

console.log(translator.translate('hello', 'John Doe')); // Bonjoure John Doe

TranslatorRemote

TranslatorRemote is a translator that is able to fetch language resources by their URL and add them to its resources.

PropertyDescription
loadResource(url, lang?, path)?fetches a resource at given url and adds it to current translator's resources.
loadResources(...urls)?fetches an array of url resources and adds it to current translator's resources.
loadLanguageResources(lang, ...urls)?fetches an array of url resources for the given lang and adds them to current translator's resources.
loadLanguagePathResources(lang, path, ...urls)?fetches an array of url resources for the given lang and path and adds them to current translator's resources.
/*
/locales/en.json
{
    "en": {
        "some": {
            "key": "Some Value"
        }
    }
}

/locales/de.json
{
    "de": {
        "some": {
            "key": "ein gewisser Wert"
        }
    }
}
*/
const translator = new TranslatorRemote();

await translator.loadResource("/locales/en.json");
await translator.loadResource("/locales/de.json");

// or

await translator.loadResources("/locales/en.json", "/locales/fr.json", "/locales/de.json");

All the load resources methods in TranslatorRemote return ServiceResponse.