Simply Translate
Simplest translations for JS. Even consider it more as an object mapper, a Dictionary, but not translation AI or Bot or something... :)
[Typescript support]
Breaking changes
(v0.20.0)
- added middleware pipeline (see Pipeline).
- added remainder (modulo) operator
%
. - added ends-with and starts-with operators
...
. - added truthy/falsy operators
!
/!!
. - added cases functionality (see Cases).
- added double curly brackets
{{...}}
support for placeholder. - deprecated
defaultLang
property overlang
name. - deprecated
$less
property. Instead of$less
useplaceholder = 'single'
. - not falling back to placeholder property name.
- removed dynamic cache.
-
deprecatedfallbackLang
propertyfallbackLang
remains. - added commonjs version (
simply-translate/commonjs
)
(v0.10.0)
-
$T{...}
replaced with$&{...}
. -
{$}
and$T{$}
removed from pluralization, use$#
instead (see Plural translations).
Install
npm i simply-translate
Import
ES6 modules
import { Translations } from 'simply-translate';
CommonJS modules
import { Translations } from 'simply-translate/commonjs';
Initialize
const dics = {...};
const translations = new Translations(dics, {lang:'en-US'});
Dictionaries
JSON with languge identifier in the root
const dics = {
"en-US": ...,
"ru-RU": ...
};
Dictionary entry
is a set of values with a unique string as a key and a string or object with value (which is required), description, and (optionally) plural and cases data.
const dics = {
'en-US': {
hello_world: 'Hello World',
goodbye_world: {
value: 'Goodbye World',
description: 'When you want to say goodbye to the world',
},
},
};
Translate
translate
or translateTo
methods.
translate
method uses lang
property of Translations
, translateTo
requires language parameter.
// create translations with dictionary:
const translations = new Translations({...}, {lang:'en-US'});
const translated = translations.translate('hello_world');
const translated = translations.translateTo('en-US', 'hello_world');
For Dynamic data use ${...}
with field name of the data object.
const dics = {
'en-US': {
hello_user: 'Hello ${user}!',
},
};
const translations = new Translations(dics, { lang: 'en-US' });
translations.translate('hello_user', { user: 'Oleg' });
// Hello Oleg!
v0.0.20 $less
is deprecated. Instead of $less
use placeholder = 'single'
.
It is required to add $ before placeholders. However it is possible to use $-less placeholders by setting placeholder
property of Translations
to single ({...}
) or double ({{...}}
) curly-braces, however it is not recommended.
const dics = {
"en-US": {
hello_user: "Hello {user}!",
},
};
const translations = new Translations(dics, { lang: "en-US", placeholder = 'single' });
translations.translate("hello_user", { user: "Oleg" }, "Hello {user}");
// Hello Oleg
// or
translations.placeholder = 'double';
translations.translate("hello_user", { user: "Oleg" }, "Hello {{user}}");
// Hello Oleg
Please note $
prefix will replace placeholder with property value from the data object, $&
translate value from data object, and &
will just translate the placeholder text.
And note: single or double placeholder ignores $
as if it is there so using just translate &
function is not available.
const dics = {
'en-US': {
hello_user: 'Hello ${user}!',
hello_user_r: 'Hello $&{usr}!',
hello_user_t: 'Hello &{usr}!',
usr: 'User',
oleg: 'Олег',
},
};
const translations = new Translations(dics, { lang: 'en-US' });
translations.translate('hello_user', { user: 'oleg' });
// Hello oleg!
translations.translate('hello_user_t', { user: 'oleg' });
// Hello User!
translations.translate('hello_user_r', { user: 'oleg' });
// Hello Олег!
Namespaces
Group items in dictionary.
const dics = {
'en-US': {
user: {
hello_user: 'Hello ${user}!',
goodbye_user: { value: 'Goodbye ${user}!' },
},
},
};
const translations = new Translations(dics, { lang: 'en-US' });
translations.translate('user.hello_user', { user: 'Oleg' });
// Hello Oleg!
translations.translate(['user', 'goodbye_user'], { user: 'Oleg' });
// Goodbye Oleg!
You don't need to directly point to value
, it is done by default.
Do not use namespaces separator (.
) for dictionary keys.
Fallback value
If value is not found and fallback
is not provided, key will be used as value.
const dics = {
'en-US': {
hello_world: 'Hello World',
},
};
const translations = new Translations(dics, { lang: 'en-US' });
translations.translate('hello_user}', { user: 'Oleg' }, 'Hello ${user}');
// Hello Oleg!
You may use ${...}
in keys, however it is not required, but might be useful.
const dics = {
'en-US': {
'hello_${user}': 'Hello ${user}!',
},
};
const translations = new Translations(dics);
translations.translateTo('en-US', 'hello_${user}', { user: 'Oleg' });
// Hello Oleg!
translations.translateTo('en-US', 'goodbye_${user}', { user: 'Oleg' });
// goodbye_Oleg!
It is possible to use fallback values for dynamic fields. Note: Due to implementation limitations (and keep library clean of dependencies) only Latin characters supported for placeholders and fallback.
Since ver.0.20.0 if property is null or undefined placeholder will be empty (not property name as it was).
const dics = {
'en-US': {
'hello_${user}': 'Hello ${user?User}!',
},
};
const translations = new Translations(dics, { lang: 'en-US' });
translations.translate('hello_user', { user: undefined });
// Hello User!
translations.translate('hi_user', { user: undefined }, 'Hi ${user?Friend}');
// Hi Friend!
translations.translate('hi_user', { user: undefined }, 'Hi ${user}');
// Hi !
Next will fail to replace placeholder:
translations.translateTo('ru-RU', 'hi_${user}', { user: 'Олег' }, 'Привет ${user?Пользователь}');
// Привет ${user?Пользователь}
To solve this add translation term:
translations.extendDictionary('ru-RU', {
User: 'Пользователь',
});
translations.translateTo('ru-RU', 'hi_${user}', { user: undefined }, 'Привет $&{user?User}');
// Привет Пользователь
Fallback language
Fallback language will use dictionary if selected language does not contain translation. Fallback dictionary will be used before fallback value.
const dics = {
'en-US': {
'hello_${user}': 'Hello ${user?User}!',
'goodbye_${user}': 'Goodbye ${user?User}!',
},
'ru-RU': {
'hello_${user}': 'Привет, $&{user}!',
user: 'Пользовтель',
Oleg: 'Олег',
},
};
const translations = new Translations(dics, {
lang: 'ru-RU',
fallbackLang: 'en-US',
});
translations.translate('hello_${user}', { user: 'Oleg' });
// Привет, Олег!
translations.translate('goodbye_${user}', { user: 'Oleg' }, 'Bye ${user?User}');
// Goodbye Олег!
translations.translate('nice_day_${user}', { user: undefined }, 'Have a nice day ${user?Friend}');
// Have a nice day Friend
Pluralization
Use $#
in plural options to insert number.
plural
property of translation value used for pluralization. Execution order is sequential.
The structure of pluralization entry is a tuple: [operation, value]
.
Supported operators: truthy/falsy !!
/!
, compare: >
,<
,=
,<=
,>=
; and few more: in []
between
, %
, ...
, and _
for default. Operations can only be done with static numbers provided in operation
.
Please note: remainder operator %
compares remainder (or modulo) operation result with 0. It is possible to use %
with specific remainder: %2=0
.
let translations = new Translations(
{
'en-US': {
'i-ate-eggs-bananas-dinner': {
value: 'I ate ${bananas} and ${eggs} for dinner',
plural: {
bananas: [
['<= 0', 'no bananas'],
['...2', 'number of bananas that ends with 2'],
['= 1', 'one banana'],
['in [3,4]', 'few bananas'],
['% 11', 'many bananas that is divisible by eleven'],
['> 10', 'too many bananas'],
['>= 5', 'many bananas'],
],
eggs: [
['= 0', 'zero eggs'],
['= 1', 'one egg'],
['between 2 and 4', 'some eggs'],
['_', '$# eggs'],
],
},
},
},
},
{
lang: 'en-US',
}
);
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 0,
eggs: 1,
});
// I ate no bananas and one egg for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 3,
eggs: 5,
});
// I ate few bananas and 5 eggs for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 1,
eggs: 1,
});
// I ate one banana and one egg for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 13,
eggs: 0,
});
// I ate too many bananas and zero eggs for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 121,
eggs: 3,
});
// I ate many bananas that is divisible by eleven and some eggs for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 6,
eggs: 3,
});
// I ate many bananas and some eggs for dinner
translations.translate('i-ate-eggs-bananas-dinner', {
bananas: 12,
eggs: one,
});
// I ate number of bananas that ends with 2 and one eggs for dinner
Plural translations
In case if dynamic parameters have to be translated you can use $&{$#}
syntax.
It is possible to modify plural translations a little bit like so: $&{my-$#-value}
.
In rare cases you are able to use dynamic replacement ${...}
placeholders as well.
let translations = new Translations(
{
'en-US': {
'i-ate-apples-for': {
value: 'I ate ${apples} for $&{when}',
plural: {
apples: [
['= 1', '&{$#-only} apple'],
['in [2,3]', '&{$#} apples'],
['= 5', '$# ($&{yay}) apples'],
['_', '$# apple(s)'],
],
},
},
dinner: 'Dinner',
breakfast: 'Breakfast',
'1-only': 'Only One',
1: 'One',
2: 'Two',
3: 'Three',
wow: 'WOW!',
},
},
{
lang: 'en-US',
}
);
translations.translate('i-ate-apples-for', {
apples: 1,
when: 'dinner',
});
// I ate Only One apple for Dinner
translations.translate('i-ate-apples-for', {
apples: 2,
when: 'breakfast',
});
// I ate Two apples for Breakfast
translations.translate('i-ate-apples-for', {
apples: 4,
when: 'breakfast',
});
// I ate Two apples for Breakfast
translations.translate('i-ate-apples-for', {
apples: 5,
when: 'breakfast',
yay: 'wow',
});
// I ate 5 (WOW!) apples for Breakfast
Cases
(v0.20.0+)
(experimental)
Similar to pluralization and executes before pluralization. Supports a bit less operators: truthy/falsy, compare and end/startsWith ...
operators.
Little bit different syntax placeholder, similar to translations but instead of &
use !
: $!{...}
.
Use replace pattern $#
in combination with $
or &
and pluralization.
let translations = new Translations({
'en-US': {
somebody_ate_bananas: {
value: '$!{prefix}${person} ate bananas',
cases: {
prefix: [
['!!', '&{$#} '],
['!', ''],
],
},
},
sir: 'Sir',
madam: 'Madam',
},
});
translations.translate('somebody_ate_bananas', {
prefix: 'sir',
person: 'Holmes',
});
// Sir Holmes ate bananas
translations.translate('somebody_ate_bananas', {
person: 'Holmes',
});
// Holmes ate bananas
let translations = new Translations({
'en-US': {
i_have_been_here_count: {
value: '$!{count} ${days}',
cases: {
count: [
['== 0', 'I have not been here'],
['_', "I've been here ${count}"],
],
},
plural: {
count: [
['=1', 'once'],
['=2', 'twice'],
['in [3,4,5]', 'few times'],
['>10', 'many times'],
['_', '$# times'],
],
days: [
['<2', 'today'],
['<5', 'for last few days'],
['_', 'for long time'],
],
},
},
},
});
translations.translate('i_have_been_here_count', {
count: 0,
days: 1,
});
// I have not been here today
translations.translate('i_have_been_here_count', {
count: 2,
days: 3,
});
// I've been here twice for last few days
As you can see this is pretty simple but may bring some value for conditional placeholders. Still figuring value of this out...
Operators
- Truthy/Falsy
!!
/!
:['!!','appear if value is truthy']
/['!','appear if value is falsy']
- Compare
>
,<
,=
,<=
,>=
. Just regular compare operators:['<2','appear if value is less then 2']
- In
in []
:['in [2,4,8]', 'only for 2, 4, and 8']
. Works only for pluralization and with digits. - Between
between
:['between 2 and 5', 'for 2, 3, 4, and 8']
. Works only for pluralization and with digits. - Remainder
%
:['%2', 'remainder that equals to 0 left over when divided by 2']
,['%3=5', 'remainder that equals to 5 when divided by 3']
- Ends/Starts with
...
operators:['...2', 'ends with 2']
,['2...', 'starts with 2']
- and
_
for default. Operators uses static values provided inoperation
.
Operators inplural
orcases
executes in the order in which it is listed, so it is important to next rule is not prevented by current, especially default_
.
Add terms to dictionary
To extend dictionary with new values use extendDictionary
method.
translations.extendDictionary('en-US', {
fruits: {
'i-ate-mango': {
value: 'I ate ${mango}',
plural: {
apples: [
['< 1', 'no mangos'],
['= 1', 'one mango'],
['_', '$# mangos'],
],
},
},
mango: 'mango',
},
tools: {
fork: 'fork',
},
});
Pipeline
(v0.20.0+)
(experimental)
To manage translation flow now there is a pipeline functionality that runs middlewares.
Default flow is the same, but now it is possible to add custom middlewares to the flow or build custom one from the scratch.
At the moment there is SimpleDefaultPipeline
which is the old one with fallback language which will be removed in future. And SimplePipeline
with access to middlewares collection.
In the Middleware
you have access to execution Context
. result
property contains value
that is going to be finial result of the entire flow. And params
is the accepted data to be used in the flow. It is intended to be readonly.
const pipeline = new SimplePipeline();
pipeline.addMiddleware((context) => {
const { params, result } = context;
if (result.fallingBack) {
// do some logic here for NOT translated values
console.warn(`the value for ${params.key} is not translated`);
result.value = `!WARNING: ${result.value} [${params.key}]`;
} else {
// do some logic here for translated values
result.value = `${result.value}: YAY!`;
}
});
let translations = new Translations(..., pipeline);
addMiddleware
adds middleware to the end of the pipeline queue. addMiddlewareAt
and removeMiddlewareAt
adds middleware at the index.