'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;