// Copyright 2019 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (dev@campbellcrowley.com) const fs = require('fs'); delete require.cache[require.resolve('./Locale.js')]; const Locale = require('./Locale.js'); /** * @description Static strings for Pets. */ class Strings { /** * @description Strings. * @param {string} [filename='global'] Filename to read strings from each * locale. Excluding path and extension. * @param {string} [dir='../../strings/'] Path to find folder of available * locales, relative to this file. * @param {string} [defaultLocale='en_US'] Default and fallback locale to use * when unspecified or no string in given locale is found. */ constructor( filename = 'global', dir = '../../strings/', defaultLocale = 'en_US') { if (typeof filename !== 'string') { throw new TypeError('Filename is not a string.'); } if (typeof dir !== 'string') { throw new TypeError('Directory is not a string.'); } defaultLocale = Strings.parseLocale(defaultLocale); if (!defaultLocale) { throw new TypeError('Default Locale is not a valid locale.'); } /** * @description Path to directory storing locale information. * @private * @type {string} * @default '../../strings/' * @constant */ this._stringsDir = dir; /** * @description Filename in locale directory to read strings from. * @private * @type {string} * @default '/global.js' * @constant */ this._stringsFilename = `/${filename}.js`; /** * @description Default and fallback locale. * @private * @type {string} * @default 'en_US' * @constant */ this._stringsDefault = `${defaultLocale.language}_${defaultLocale.territory}`; /** * @description Reference to default locale. This is used when a string key * is unable to be found in a locale, or the locale doesn't exist. * @public * @type {Strings~Locale} * @default */ this.defaultLocale = require( `${this._stringsDir}${this._stringsDefault}${this._stringsFilename}`); this.get = this.get.bind(this); } /** * @description Regular Expression to match a valid locale. Attempts to * conform to ISO/IEC 15897. Does not accept modifier. */ static get localRegExp() { return new RegExp( '^(?<language>[a-z]{2})(?:_(?<territory>[A-Z]{2}))?' + '(?:\\.(?<codeset>[^@]+))?$'); } /** * @description Parse the given string as a locale. * @public * @static * @param {string} locale The locale to parse. * @returns {?{ * language: string, * territory: ?string, * codeset: ?string * }} Matched groups or null if not a valid locale. */ static parseLocale(locale) { const match = typeof locale === 'string' && locale.match(Strings.localRegExp); return match && match.groups; } /** * @description Purge all strings from memory to force them to be reloaded. * Asynchronous. Does not complete immediately. * @public */ purge() { fs.readdir(`${__dirname}/${this._stringsDir}`, (err, files) => { if (err) { console.error(err); return; } for (const f of files) { if (f.endsWith('.json')) continue; delete require.cache[require.resolve( `${this._stringsDir}${f}${this._stringsFilename}`)]; } }); } /** * @description Get the locale group of the given locale. * @public * @param {string} locale The locale to fetch. * @returns {?Locale} The locale group, or null if couldn't be found. */ getGroup(locale) { const match = Strings.parseLocale(locale || this._stringsDefault); let localeGroup = this.defaultLocale; let lang = this._stringsDefault; if (!match) { console.error(`Bad locale: ${locale}. Using default.`); } else { lang = `${match.language}_${match.territory}`; try { localeGroup = require(`${this._stringsDir}${lang}${this._stringsFilename}`); } catch (err) { console.error(`Unable to find locale: ${lang}. Using default.`, err); } } return localeGroup; } /** * @description Get and format a specific string. * * @public * @static * @param {string} key String key to find. * @param {string} [locale] Lookup the string in a specific locale. * @param {...string} [rep] Data to replace placeholders in the string. * @returns {?string} Matched and replaced string, or null if unable to find. */ get(key, locale, ...rep) { const localeGroup = this.getGroup(locale); if (!localeGroup) { console.error(`Unable to find locale: ${locale}`); return null; } return localeGroup.get(key, ...rep); } /** * @description Get but don't format a specific string. * * @public * @static * @param {string} key String key to find. * @param {string} [locale] Lookup the string in a specific locale. * @returns {?string} Matched string, or null if unable to find. */ getRaw(key, locale) { const localeGroup = this.getGroup(locale); if (!localeGroup) { console.error(`Unable to find locale: ${locale}`); return null; } return localeGroup.getRaw(key); } /** * @description Reply to msg with locale strings. * @public * * @param {Common} common Reference to Common for reply helper. * @param {Discord~Message} msg Message to reply to. * @param {?string} titleKey String key for the title, or null for default. * @param {string} bodyKey String key for the body message. * @param {string} [rep] Placeholder replacements for the body only. * @returns {Promise<Discord~Message>} Message send promise from * {@link Discord}. */ reply(common, msg, titleKey, bodyKey, ...rep) { return common.reply( msg, this.get(titleKey, msg.locale) || '', this.get(bodyKey, msg.locale, ...rep) || ''); } } Strings.Locale = Locale; module.exports = Strings;