Skip to content

Commit

Permalink
Add string localization loading and fetching to Lang
Browse files Browse the repository at this point in the history
Also began localizing the help command a bit to test things out
  • Loading branch information
zajrik committed Jun 16, 2017
1 parent 4a73729 commit 52bb9ad
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 41 deletions.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"devDependencies": {
"@types/chalk": "^0.4.31",
"@types/glob": "^5.0.30",
"@types/node": "^7.0.8",
"@types/node": "^7.0.31",
"@types/node-json-db": "0.0.1",
"del": "^2.2.2",
"gulp": "^3.9.1",
Expand Down
9 changes: 8 additions & 1 deletion src/client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class Client extends Discord.Client
@logger private readonly _logger: Logger;
public readonly name: string;
public readonly commandsDir: string;
public readonly localeDir: string;
public readonly owner: string | string[];
public readonly defaultLang: string;
public readonly statusText: string;
Expand Down Expand Up @@ -102,7 +103,12 @@ export class Client extends Discord.Client
* **See:** {@link Client#passive}
* @type {string}
*/
this.commandsDir = path.resolve(options.commandsDir) || null;
this.commandsDir = options.commandsDir ? path.resolve(options.commandsDir) : null;

/**
* Directory to find custom localization files
*/
this.localeDir = options.localeDir ? path.resolve(options.localeDir) : null;

/**
* Default language to use for localization
Expand Down Expand Up @@ -221,6 +227,7 @@ export class Client extends Discord.Client
if (!(this.owner instanceof Array)) throw new TypeError('Client config `owner` field must be an array of user ID strings.');

Lang.createInstance(this);
Lang.loadLocalizations();

// Load commands
if (!this.passive)
Expand Down
4 changes: 3 additions & 1 deletion src/command/CommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export class CommandLoader<T extends Client>
if (this._client.commands.size > 0) this._client.commands.clear();
let commandFiles: string[] = [];
commandFiles.push(...glob.sync(`${path.join(__dirname, './base')}/**/*.js`));
commandFiles.push(...glob.sync(`${this._client.commandsDir}/**/*.js`));
if (this._client.commandsDir)
commandFiles.push(...glob.sync(`${this._client.commandsDir}/**/*.js`));

let loadedCommands: number = 0;
for (const fileName of commandFiles)
{
Expand Down
28 changes: 16 additions & 12 deletions src/command/base/Help.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Collection, RichEmbed } from 'discord.js';
import { LangResourceFunction } from '../../types/LangResourceFunction';
import { LocalizedCommandInfo } from '../../types/LocalizedCommandInfo';
import { Lang } from '../../localization/Lang';
import { Message } from '../../types/Message';
import { Util } from '../../util/Util';
import { Command } from '../Command';
import { Lang } from '../../localization/Lang';
import { Collection, RichEmbed } from 'discord.js';
import { Util } from '../../util/Util';

export default class extends Command
{
Expand Down Expand Up @@ -72,15 +73,18 @@ export default class extends Command
+ `not have permission to view it.`;

const info: LocalizedCommandInfo = cInfo(command);
if (command) output = '```ldif\n'
+ (command.guildOnly ? '[Server Only]\n' : '')
+ (command.ownerOnly ? '[Owner Only]\n' : '')
+ `Command: ${command.name}\n`
+ `Description: ${info.desc}\n`
+ (command.aliases.length > 0 ? `Aliases: ${command.aliases.join(', ')}\n` : '')
+ `Usage: ${command.usage}\n`
+ (info.info ? `\n${info.info}` : '')
+ '\n```';
const res: LangResourceFunction = Lang.createResourceLoader(lang);
if (command) output = res('CMD_HELP_CODEBLOCK', {
serverOnly: command.guildOnly ? res('CMD_HELP_SERVERONLY') : '',
ownerOnly: command.ownerOnly ? res('CMD_HELP_OWNERONLY') : '',
commandName: command.name,
desc: info.desc,
aliasText: command.aliases.length > 0 ?
`${res('CMD_HELP_ALIASES', { aliases: command.aliases.join(', ')})}\n`
: '',
usage: command.usage,
info: info.info ? `\n${info.info}` : ''
});
}

output = dm ? output.replace(/<prefix>/g, '')
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ export { StorageFactory } from './storage/StorageFactory';
export { JSONProvider } from './storage/JSONProvider';

import * as CommandDecorators from './command/CommandDecorators';
export { CommandDecorators }
export { CommandDecorators };

export { Time } from './util/Time';
export { Util } from './util/Util';
export { Logger } from './util/logger/Logger';
export { logger } from './util/logger/LoggerDecorator';
export { LogLevel } from './types/LogLevel';
export { Lang } from './localization/Lang';

export { deprecated } from './util/DeprecatedDecorator';

Expand Down
118 changes: 103 additions & 15 deletions src/localization/Lang.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,101 @@
import { LangResourceFunction } from '../types/LangResourceFunction';
import { LocalizedCommandInfo } from '../types/LocalizedCommandInfo';
import { TokenReplaceData } from '../types/TokenReplaceData';
import { logger, Logger } from '../util/logger/Logger';
import { Command } from '../command/Command';
import { Client } from '../client/Client';
import * as glob from 'glob';
import * as path from 'path';

/**
* Class for loading localization files and fetching localized values
* Singleton for loading localization files and fetching localized values
*/
export class Lang
{
@logger private static logger: Logger;
private static _instance: Lang;
private client: Client;
private commandInfo: { [command: string]: { [lang: string]: LocalizedCommandInfo } };
private langs: { [lang: string]: { [key: string]: string } };
private constructor(client: Client)
{
if (Lang._instance)
throw new Error('Cannot create multiple instances of Lang singleton');

this.client = client;
this.commandInfo = {};
this.langs = {};
}

/**
* Contains all loaded languages and their strings.
* This does not include localized helptext
* @type {object}
*/
public static get langs(): { [lang: string]: { [key: string]: string } }
{
return Lang._instance.langs;
}

/**
* Get all available localization languages
* @type {string[]}
*/
public static get langNames(): string[]
{
let langs: Set<string> = new Set();
for (const commandName of Object.keys(Lang._instance.commandInfo))
for (const lang of Object.keys(Lang._instance.commandInfo[commandName]))
langs.add(lang);

for (const lang of Object.keys(Lang.langs)) langs.add(lang);

return Array.from(langs);
}

/**
* Create the singleton instance
* @param {Client} client YAMDBF Client instance
* @returns {void}
*/
public static createInstance(client: Client): void
{
if (!Lang._instance) Lang._instance = new Lang(client);
}

/**
* Load localization files from the Client's `localeDir`
* @returns {void}
*/
public static loadLocalizations(): void
{
if (!Lang._instance) return;
const langNameRegex: RegExp = /\/([^\/\.]+)(?:\..+)?\.lang\.json/;

let langFiles: string[] = [];
langFiles.push(...glob.sync(`${path.join(__dirname, './en_us')}/**/*.lang.json`));
if (Lang._instance.client.localeDir)
langFiles.push(...glob.sync(`${Lang._instance.client.localeDir}/**/*.lang.json`));

for (const langFile of langFiles)
{
delete require.cache[require.resolve(langFile)];
const loadedLangFile: { [key: string]: string } = require(langFile);

if (!langNameRegex.test(langFile)) continue;
const langName: string = langFile.match(langNameRegex)[1];
if (typeof Lang._instance.langs[langName] !== 'undefined')
Lang._instance.langs[langName] = { ...Lang._instance.langs[langName], ...loadedLangFile };
else
Lang._instance.langs[langName] = loadedLangFile;
}

Lang.logger.info('Lang', `Loaded string localizations for ${Object.keys(Lang.langs).length} languages`);
}

/**
* Load any command localizations and assign them to commands
* @returns {void}
*/
public static loadCommandLocalizations(): void
{
Expand All @@ -45,26 +111,14 @@ export class Lang
catch (err) { continue; }
Lang._instance.commandInfo[command.name] = localizations;
}
}

/**
* Get all available localization languages
* TODO: Extend this to scan language string files after
* adding support for those
*/
public static getLangs(): string[]
{
let langs: Set<string> = new Set();
for (const commandName of Object.keys(Lang._instance.commandInfo))
for (const lang of Object.keys(Lang._instance.commandInfo[commandName]))
langs.add(lang);

return Array.from(langs);
Lang.logger.info('Lang', `Loaded helptext localizations for ${Lang.langNames.length} languages`);
}

/**
* Get localized command info, defaulting to the info
* given in the Command's constructor
* @returns {LocalizedCommandInfo}
*/
public static getCommandInfo(command: Command, lang: string): LocalizedCommandInfo
{
Expand All @@ -81,4 +135,38 @@ export class Lang

return { desc, info };
}

/**
* Get a string resource for the given language, replacing any
* tokens with the given data
* @param {string} lang Language to get a string resource for
* @param {string} key String key to get
* @param {TokenReplaceData} [data] Values to replace in the string
* @returns {string}
*/
public static res(lang: string, key: string, data?: TokenReplaceData): string
{
if (!Lang.langs[lang]) return key;
const strings: { [key: string]: string } = Lang.langs[lang];
let loadedString: string = strings[key];

if (!loadedString) return key;
if (typeof data === 'undefined') return loadedString;

for (const token of Object.keys(data))
loadedString = loadedString.replace(new RegExp(`{{ *${token} *}}`, 'g'), data[token]);

return loadedString;
}

/**
* Takes a language string and returns a function that loads string
* resources for that specific language
* @param {string} lang The language to create a loader for
* @returns {LangResourceFunction}
*/
public static createResourceLoader(lang: string): LangResourceFunction
{
return (key: string, data?: TokenReplaceData) => Lang.res(lang, key, data);
}
}
6 changes: 6 additions & 0 deletions src/localization/en_us/en_us.cmd.help.lang.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"CMD_HELP_SERVERONLY": "[Server Only]",
"CMD_HELP_OWNERONLY": "[Owner Only]",
"CMD_HELP_ALIASES": "Aliases: {{aliases}}",
"CMD_HELP_CODEBLOCK": "```ldif\n{{serverOnly}}{{ownerOnly}}Command: {{commandName}}\nDescription: {{desc}}\n{{aliasText}}Usage: {{usage}}\n{{info}}\n```"
}
8 changes: 8 additions & 0 deletions src/types/LangResourceFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @typedef {Function} LangResourceFunction Represents a function assigned to
* a specific language that takes a string key and a {@link TokenReplaceData}
* object and returns a localized string for that language if it exists
*/

import { TokenReplaceData } from './TokenReplaceData';
export type LangResourceFunction = (key: string, data?: TokenReplaceData) => string;
8 changes: 8 additions & 0 deletions src/types/TokenReplaceData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @typedef {object} TokenReplaceData Represents an object mapping key
* tokens to string values, where the token keys will be replaced with
* the provided value in the source string when given to a Lang resource
* function like {@link Lang.res}
*/

export type TokenReplaceData = { [key: string]: string };
2 changes: 2 additions & 0 deletions src/types/YAMDBFOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @property {string[]} [owner=[]] Can also be a single string<br>See: {@link Client#owner}
* @property {string} [provider] See: {@link Client#provider}
* @property {string} [commandsDir] See: {@link Client#commandsDir}
* @property {string} [localeDir] See: {@link Client#localeDir}
* @property {string} [defaultLang] See: {@link Client#defaultLang}
* @property {string} [statusText=null] See: {@link Client#statusText}
* @property {string} [readyText='Client ready!'] See: {@link Client#readyText}
Expand All @@ -29,6 +30,7 @@ export type YAMDBFOptions = {
owner?: string | string[];
provider?: StorageProviderConstructor;
commandsDir?: string;
localeDir?: string;
defaultLang?: string;
statusText?: string;
readyText?: string;
Expand Down
6 changes: 6 additions & 0 deletions test/locale/al_bhed/al_bhed.cmd.help.lang.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"CMD_HELP_SERVERONLY": "[Canjan Uhmo]",
"CMD_HELP_OWNERONLY": "[Ufhan Uhmo]",
"CMD_HELP_ALIASES": "Ymeycac: {{aliases}}",
"CMD_HELP_CODEBLOCK": "```ldif\n{{serverOnly}}{{ownerOnly}}Lussyht: {{commandName}}\nTaclnebdeuh: {{desc}}\n{{aliasText}}Icyka: {{usage}}\n{{info}}\n```"
}
1 change: 1 addition & 0 deletions test/test_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Test extends Client
token: config.token,
owner: config.owner,
commandsDir: './commands',
localeDir: './locale',
defaultLang: 'al_bhed',
pause: true,
logLevel: LogLevel.DEBUG,
Expand Down

0 comments on commit 52bb9ad

Please sign in to comment.