Hermes is a lightweight and simple to use package for managing i18n strings for Node.js applications. It is designed to be modular and flexible, allowing you to split your translations into multiple files and directories, and to use a simple formatting syntax to insert variables into your strings.
To install Hermes with npm, run:
npm install "@fca.gg/hermes"
Clone the repository and run the following command to install the dependencies:
npm install
To run the tests, use the following command:
npm run test
In order to use Hermes, you will need to create a new directory to store your translations. Inside this directory, you can create as many JSON files as you like, each representing a different language. For example, you might have the following directory structure:
translations/
en-US.json
fr.json
de.json
Note
Hermes supports JSON5 parsing. This means you can use comments, trailing commas, and other features that are not supported in standard JSON.
You can use the JSON5 syntax out of the box in any .json
files, but for standards it is recommended to use the .json5
extension when using the more recent syntax.
For languages list:
export enum Langs {
INDONESIAN = 'id',
DANISH = 'da',
GERMAN = 'de',
ENGLISH_UK = 'en-GB',
ENGLISH_US = 'en-US',
SPANISH = 'es-ES',
FRENCH = 'fr',
CROATIAN = 'hr',
ITALIAN = 'it',
LITHUANIAN = 'lt',
HUNGARIAN = 'hu',
DUTCH = 'nl',
NORWEGIAN = 'no',
POLISH = 'pl',
BRAZILIAN_PORTUGUESE = 'pt-BR',
ROMANIAN = 'ro',
FINNISH = 'fi',
SWEDISH = 'sv-SE',
VIETNAMESE = 'vi',
TURKISH = 'tr',
CZECH = 'cs',
GREEK = 'el',
BULGARIAN = 'bg',
RUSSIAN = 'ru',
UKRAINIAN = 'uk',
HINDI = 'hi',
THAI = 'th',
CHINESE_CHINA = 'zh-CN',
JAPANESE = 'ja',
CHINESE_TAIWAN = 'zh-TW',
}
Each of these files should contain a JSON object with key-value pairs representing the translations for that language. For example, the contents of en-US.json
might look like this:
{
"hello": "Hello World!",
// You can also nest objects
"nested": {
"key": "This is a nested key"
}
}
To initialize Hermes, you will need to import the Hermes
class and call the init()
method. This method can take an optional configuration object with the following properties:
translationsDir
: The path to the directory containing your translation files. Defaults to./translations
.noMissingTranslations
: Can be set towarn
,throw
, orignore
. Defaults towarn
.noEmptyTranslations
: Can be set towarn
,throw
, orignore
. Defaults towarn
.fallbackLanguage
: The language to use as a fallback if a language code is not found. Does not have a default value.
import { Hermes } from '@fca.gg/hermes';
async function main() {
await Hermes.init()
// Do something with Hermes
}
main()
In order to get a translation, you need to fetch the specific context of a language. For example, if you want to get the translation for the greeting
key in the en-US
context, you would do the following:
import Hermes from '@fca.gg/hermes';
const ctx = Hermes.getContext('en-US');
ctx.t('hello'); // returns "Hello World!"
// You can also get nested keys by using dot notation
ctx.t('nested.key'); // returns "This is a nested key"
Warning
If you try to access a key that does not exist, Hermes will throw an error. Therefore, ensure that all keys are present in all translation files.
By default warnings are logged if a key is missing on init, but you can change this behavior by setting the noMissingTranslations
option in the init()
method.
When defining your translations, you can include variables and expressions inside your string by wrapping them in %
.
For example, you might have the following translation:
{
"greeting": "Hello, %name%!",
"condition": "It is %temp% degrees outside. %isHot:if(Wow that's hot...)%"
}
You can then pass an object with the variables you want to replace in the t
method:
import Hermes from '@fca.gg/hermes';
const ctx = Hermes.getContext('en-US');
ctx.t('greeting', { name: 'Alice' }); // returns "Hello, Alice!"
ctx.t('condition', { temp: 30, isHot: true }); // returns "It is 30 degrees outside. Wow that's hot..."
// (We're using celcius here)
You can extend the functionality of variables by using expressions. A conditional expression was used in the previous example, but you can also use expressions to format dates, numbers, and more:
%boolValue:if(text)%
: IfboolValue
is true, includetext
in the output. Otherwise, ignore it.%boolValue:either(text|other text)%
: IfboolValue
is true, includetext
in the output. Otherwise, includeother text
.%dateValue:date(YY-MM-DD)%
: FormatdateValue
as a date using the specified format. You can use any valid date format string.YY
: 2-digit yearMM
: 2-digit monthDD
: 2-digit dayYYYY
: 4-digit yearhh
: 2-digit hourmm
: 2-digit minutess
: 2-digit second
%value:switch(case:value)%
: Switch on the value ofvalue
and return the corresponding case. You can specify multiple cases separated by|
.%value:switch(text value:This is a text value)
: If value is "text value", return "This is a text value".%value:switch(45:The value is 45)%
: If value is 45, return "The value is 45".%value:switch(45-60:The value is between 45 and 60)%
: If value is between 45 and 60 (exclusive), return "The value is between 45 and 60". You can also put-inf
orinf
to match all values below or above a certain value. Inputting a!
after a number will include that number in the range. For example,45-60!
will include all numbers between 45 and 60, including 60.
You can also define a scope for your translations by specifying it in the getContext()
method. This allows you to group your translations by a specific context, such as a module or component name. For example:
import Hermes from '@fca.gg/hermes';
const ctx = Hermes.getContext('en-US', 'nested');
ctx.t('key'); // returns "This is a nested key"
Hermes provides options for handling missing and empty translations. By default, Hermes will log a warning if a translation is missing or empty. You can change this behavior by setting the noMissingTranslations
and noEmptyTranslations
options in the init()
method. For example:
import Hermes from '@fca.gg/hermes';
async function main() {
await Hermes.init({
noMissingTranslations: 'throw',
noEmptyTranslations: 'ignore'
});
// Do something with Hermes
}
main()
You can also nest files in directories to group your translations. For example, you might have the following directory structure:
translations/
en-US.json
fr.json
en-GB/
common.json
auth.json
features/
feature1.json
feature2.json
In this case, you can access keys in the nested files by using dot notation:
import Hermes from '@fca.gg/hermes';
const ctx = Hermes.getContext('en-GB');
ctx.t('common.key'); // returns the value of the key in common.json
ctx.t('features.feature1.key'); // returns the value of the key in feature1.json
You can also get an object composed of all the translations for a specific key in all languages by using the getLocalizedObject()
method. For example:
import Hermes from '@fca.gg/hermes';
// This is only a definition of the LocalizedObject type
type LocalizedObject = Partial<
[key: Langs]: string
> // AKA
type LocalizedObject = Partial<Record<Langs, string>>
const obj = Hermes.getLocalizedObject('key');
This project is licensed under the AGPL v3 License - see the LICENSE file for details.
We chose the AGPL to ensure that Hermes remains truly open source and contributive. If you use or adapt Hermes, even over a network, you must share your modifications. That’s the spirit of the project — building useful tools together, in the open.