managers_TranslationManager.js
'use strict';
const readDirectory = require('node:util').promisify(require('node:fs').readdir);
const statDirectory = require('node:util').promisify(require('node:fs').stat);
const i18next = require('i18next');
const i18nextBackend = require('i18next-fs-backend');
const { DisGroupDevError, Messages } = require('../errors/DisGroupDevError');
/**
* @typedef {Object} TranslationManagerOptions
* @property {String} defaultLanguage The default language to use if no language is specified.
* @property {i18next.InitOptions} i18nextOptions Additional options to pass to i18next.
* @property {String} locationTranslations The location of the translations
*/
/**
* The translation manager.
* @class
*/
class TranslationManager {
/**
* The constructor of the TranslationManager class.
* @param {TranslationManagerOptions} options The options of the translation manager
*/
constructor(options) {
if (!options.locationTranslations || typeof options.locationTranslations !== 'string') throw new DisGroupDevError(Messages.INVALID_LOCATION);
/**
* The namespaces of the translations.
* @type {?Array<String>}
* @private
*/
this._namespaces = null;
/**
* The map with all translations.
* @type {?Map<String, Function>}
* @private
*/
this._translations = null;
/**
* The options of the TranslationManager.
* @type {TranslationManagerOptions}
* @public
*/
this.options = options;
/**
* If the translations are loaded.
* @type {Boolean}
* @public
*/
this.isReady = false;
this._init();
}
/**
* Inits the TranslationManager.
* @returns {Promise<void>}
* @private
*/
async _init() {
const { namespaces, totalLanguages } = await this._loadAll();
i18next.use(new i18nextBackend());
await i18next.init({
backend: {
jsonIndent: 4,
loadPath: require('node:path').resolve(this.options.locationTranslations, './{{lng}}/{{ns}}.json'),
},
fallbackLng: this.options.defaultLanguage ?? 'en-US',
initImmediate: false,
interpolation: { escapeValue: false },
load: 'all',
ns: namespaces,
preload: totalLanguages,
...this.options.i18nextOptions,
});
this._namespaces = namespaces;
this._translations = new Map(totalLanguages.map(language => [language, i18next.getFixedT(language)]));
this.isReady = true;
}
/**
* @typedef {Object} LoadAllOptions
* @property {String} [folderName] The name of the folder
* @property {String} [translationDir] The location of the translations
* @property {Array} [namespaces] The namespaces
*/
/**
* Loads all translations
* @param {LoadAllOptions} options The options for loading all translations
* @returns {Promise<Object<Array<Set>, Array>>}
* @private
*/
async _loadAll(options = { folderName: '', namespaces: [], translationDir: this.options.locationTranslations }) {
const translationDirectory = await readDirectory(options.translationDir);
let totalLanguages = [];
for (const translationFile of translationDirectory) {
const translationStat = await statDirectory(require('node:path').resolve(options.translationDir, translationFile));
if (translationStat.isDirectory()) {
const isLanguage = translationFile.includes('-');
if (isLanguage) totalLanguages.push(translationFile);
const translationFolder = await this._loadAll({
folderName: isLanguage ? '' : `${translationFile}/`,
namespaces: options.namespaces,
translationDir: require('node:path').join(options.translationDir, translationFile),
});
options.namespaces = translationFolder.namespaces;
} else { options.namespaces.push(`${options.folderName}${translationFile.substring(0, translationFile.length - 5)}`); }
}
return { namespaces: [...new Set(options.namespaces)], totalLanguages };
}
/**
* Deletes a translation
* @param {String} name The name of the translation
* @returns {Map<String, Function>|null}
* @public
*/
delete(name) {
if (typeof name !== 'string') throw new DisGroupDevError(Messages.NOT_A_STRING(name));
if (!this._translations.has(name)) return null;
this._translations.delete(name);
return this._translations;
}
/**
* Gets a translation
* @param {String} name The name of the translation
* @returns {Function|null}
* @public
*/
get(name) {
if (typeof name !== 'string') throw new DisGroupDevError(Messages.NOT_A_STRING(name));
return this._translations.get(name) ?? null;
}
/**
* Checks if a translation exists
* @param {String} name The name of the translation
* @returns {Boolean}
* @public
*/
has(name) {
if (typeof name !== 'string') throw new DisGroupDevError(Messages.NOT_A_STRING(name));
return this._translations.has(name);
}
/**
* Lists all translations
* @returns {Function[]|null}
* @public
*/
list() {
return [...this._translations.values()];
}
/**
* Sets a translation
* @param {String} name The name of the translation
* @param {Function} value The function of the translation (<i18next>.getFixedT)
* @returns {?Map<String, Function>}
* @public
*/
set(name, value) {
if (typeof name !== 'string') throw new DisGroupDevError(Messages.NOT_A_STRING(name));
if (typeof value !== 'function') throw new DisGroupDevError(Messages.NOT_A_FUNCTION(value));
return this._translations.set(name, value);
}
/**
* Gets the number of translations
* @returns {Number}
* @public
*/
size() {
return this._translations.size;
}
/**
* @typedef {Object} TranslateOptions
* @property {String} [language] The language
*/
/**
* Translates a string
* @param {String} key The key
* @param {Object} [args={}] The args
* @param {TranslateOptions} options The options
* @returns {String|null}
* @public
*/
translate(key, args = {}, options = { language: this.options.defaultLanguage }) {
if (!this.isReady) throw new DisGroupDevError(Messages.NOT_READY);
return this.get(options.language)(key, args) ?? null;
}
}
module.exports = TranslationManager;