Skip to content

Commit

Permalink
Translation Caching (#44), code cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
WillTDA committed Jun 17, 2023
1 parent bfef8a7 commit ef876ff
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 29 deletions.
57 changes: 36 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const translate = require("./translate");
const awaitInput = require("./input");
const attemptingGuess = new Set();

//this simply gets the user's reply from a button interaction (that is, if the user has chosen to enable buttons)
//helper function to get the user's reply from a button interaction
function getButtonReply(interaction) {
interaction = interaction.customId;
if (interaction === "✅") return "y"; //yes
Expand All @@ -18,6 +18,21 @@ function getButtonReply(interaction) {
else return null;
};

/**
* Akinator Game Options
* @typedef {object} gameOptions
* @prop {string} [options.language="en"] The language of the game. Defaults to `en`.
* @prop {boolean} [options.childMode=false] Whether to use Akinator's Child Mode. Defaults to `false`.
* @prop {"character" | "animal" | "object"} [options.gameType="character"] The type of Akinator game to be played. Defaults to `character`.
* @prop {boolean} [options.useButtons=false] Whether to use Discord's buttons instead of message input. Defaults to `false`.
* @prop {Discord.ColorResolvable} [options.embedColor="Random"] The color of the message embeds. Defaults to `Random`.
* @prop {object} [translationCaching={}] The options for translation caching.
* @prop {boolean} [translationCaching.enabled=true] Whether to cache successful translations in a JSON file to reduce API calls and boost performance. Defaults to `true`.
* @prop {string} [translationCaching.path="./translationCache"] The path to the directory where the translation cache files are stored. Defaults to `./translationCache`.
*
* __Note:__ Paths are relative to the current working directory. (`process.cwd()`)
*/

/**
* Play a Game of Akinator.
*
Expand All @@ -30,31 +45,32 @@ function getButtonReply(interaction) {
* - `gameType` - The type of Akinator game to be played. (`animal`, `character` or `object`)
* - `useButtons` - Whether to use Discord's buttons instead of message input.
* - `embedColor` - The color of the message embeds.
* - `cacheTranslations` - Whether to cache translations to reduce API calls and boost performance.
* - `cacheTranslations` - Whether to cache translations in a JSON file to reduce API calls and boost performance.
*
* @param {Discord.Message | Discord.CommandInteraction} input The Message or Slash Command Sent by the User.
* @param {object} options The Options for the Game.
* @param {string} [options.language="en"] The language of the game. Defaults to "en".
* @param {boolean} [options.childMode=false] Whether to use Akinator's Child Mode. Defaults to "false".
* @param {"character" | "animal" | "object"} [options.gameType="character"] The type of Akinator game to be played. Defaults to "character".
* @param {boolean} [options.useButtons=false] Whether to use Discord's buttons instead of message input. Defaults to "false".
* @param {Discord.ColorResolvable} [options.embedColor="Random"] The color of the message embeds. Defaults to "Random".
* @param {Discord.Message | Discord.CommandInteraction} input The Message or Slash Command sent by the user.
* @param {gameOptions} options The options for the game.
* @returns {Promise<void>} Discord.js Akinator Game
*/

module.exports = async function (input, options = {}) {
module.exports = async function (input, options) {
//check discord.js version
if (Discord.version.split(".")[0] < 14) return console.log(`Discord.js Akinator Error: Discord.js v14 or later is required.\nPlease check the README for finding a compatible version for Discord.js v${Discord.version.split(".")[0]}\nNeed help? Join our Discord server at 'https://discord.gg/P2g24jp'`);

let inputData = {};
try {
//TODO: Data type validation
//configuring game options if not specified
options.language = options.language || "en";
options.childMode = options.childMode || false;
options.childMode = options.childMode !== undefined ? options.childMode : false;
options.gameType = options.gameType || "character";
options.useButtons = options.useButtons || false;
options.useButtons = options.useButtons !== undefined ? options.useButtons : false;
options.embedColor = Discord.resolveColor(options.embedColor || "Random");

//configuring translation caching options if not specified
options.translationCaching = options.translationCaching || {};
options.translationCaching.enabled = options.translationCaching.enabled !== undefined ? options.translationCaching.enabled : true;
options.translationCaching.path = options.translationCaching.path || "./translationCache";

options.language = options.language.toLowerCase();
options.gameType = options.gameType.toLowerCase();

Expand Down Expand Up @@ -121,7 +137,7 @@ module.exports = async function (input, options = {}) {

let akiEmbed = {
title: `${translations.question} ${aki.currentStep + 1}`,
description: `**${translations.progress}: 0%\n${await translate(aki.question, options.language)}**`,
description: `**${translations.progress}: 0%\n${await translate(aki.question, options.language, options.translationCaching)}**`,
color: options.embedColor,
fields: [],
author: { name: usertag, icon_url: avatar }
Expand Down Expand Up @@ -154,8 +170,8 @@ module.exports = async function (input, options = {}) {
hasGuessed = true;

let guessEmbed = {
title: `${await translate(`I'm ${Math.round(aki.progress)}% sure your ${options.gameType} is...`, options.language)}`,
description: `**${aki.answers[0].name}**\n${await translate(aki.answers[0].description, options.language)}\n\n${options.gameType == "animal" ? translations.isThisYourAnimal : options.gameType == "character" ? translations.isThisYourCharacter : translations.isThisYourObject} ${!options.useButtons ? `**(Type Y/${translations.yes} or N/${translations.no})**` : ""}`,
title: `${await translate(`I'm ${Math.round(aki.progress)}% sure your ${options.gameType} is...`, options.language, options.translationCaching)}`,
description: `**${aki.answers[0].name}**\n${await translate(aki.answers[0].description, options.language, options.translationCaching)}\n\n${options.gameType == "animal" ? translations.isThisYourAnimal : options.gameType == "character" ? translations.isThisYourCharacter : translations.isThisYourObject} ${!options.useButtons ? `**(Type Y/${translations.yes} or N/${translations.no})**` : ""}`,
color: options.embedColor,
image: { url: aki.answers[0].absolute_picture_path },
author: { name: usertag, icon_url: avatar },
Expand All @@ -168,7 +184,7 @@ module.exports = async function (input, options = {}) {
await akiMessage.edit({ embeds: [guessEmbed] });
akiMessage.embeds[0] = guessEmbed;

await awaitInput(options.useButtons, inputData, akiMessage, true, translations, options.language)
await awaitInput(options.useButtons, inputData, akiMessage, true, translations, options.language, options.translationCaching)
.then(async response => {
if (response === null) {
notFinished = false;
Expand Down Expand Up @@ -227,7 +243,7 @@ module.exports = async function (input, options = {}) {
if (updatedAkiEmbed !== akiMessage.embeds[0]) {
updatedAkiEmbed = {
title: `${translations.question} ${aki.currentStep + 1}`,
description: `**${translations.progress}: ${Math.round(aki.progress)}%\n${await translate(aki.question, options.language)}**`,
description: `**${translations.progress}: ${Math.round(aki.progress)}%\n${await translate(aki.question, options.language, options.translationCaching)}**`,
color: options.embedColor,
fields: [],
author: { name: usertag, icon_url: avatar }
Expand All @@ -241,7 +257,7 @@ module.exports = async function (input, options = {}) {
akiMessage.embeds[0] = updatedAkiEmbed
}

await awaitInput(options.useButtons, inputData, akiMessage, false, translations, options.language)
await awaitInput(options.useButtons, inputData, akiMessage, false, translations, options.language, options.translationCaching)
.then(async response => {
if (response === null) {
await aki.win()
Expand Down Expand Up @@ -271,7 +287,7 @@ module.exports = async function (input, options = {}) {

let thinkingEmbed = {
title: `${translations.question} ${aki.currentStep + 1}`,
description: `**${translations.progress}: ${Math.round(aki.progress)}%\n${await translate(aki.question, options.language)}**`,
description: `**${translations.progress}: ${Math.round(aki.progress)}%\n${await translate(aki.question, options.language, options.translationCaching)}**`,
color: options.embedColor,
fields: [],
author: { name: usertag, icon_url: avatar },
Expand Down Expand Up @@ -310,8 +326,7 @@ module.exports = async function (input, options = {}) {
}
} catch (e) {
//log any errors that come
attemptingGuess.delete(inputData.guild.id)
if (e == "DiscordAPIError: Unknown Message") return;
//attemptingGuess.delete(inputData.guild.id)
console.log("Discord.js Akinator Error:")
console.log(e);
}
Expand Down
5 changes: 3 additions & 2 deletions src/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ const translate = require("./translate");
* @param {boolean} isGuessFilter Specifies whether to only show buttons used when Akinator is guessing.
* @param {any} translations Active translation file.
* @param {string} language The language of the game.
* @param {boolean} translationCachingOptions Whether to cache the translation in JSON or not.
*
*/

module.exports = async function awaitInput(useButtons, input, botMessage, isGuessFilter, translations, language) {
module.exports = async function awaitInput(useButtons, input, botMessage, isGuessFilter, translations, language, translationCachingOptions) {
//check if useButtons is true. If so, use buttons. If not, use text input
if (useButtons) {
let yes = { type: 2, label: translations.yes, style: 2, custom_id: "✅", emoji: { name: "✅" } }
Expand Down Expand Up @@ -82,7 +83,7 @@ module.exports = async function awaitInput(useButtons, input, botMessage, isGues
await response.first().delete();
const responseText = String(response.first()).toLowerCase();
if (["y", "n", "i", "idk", "p", "pn", "b", "s"].includes(responseText)) return responseText; //skip translation for these responses
return await translate(responseText, language);
return await translate(responseText, language, translationCachingOptions);
}

}
Expand Down
48 changes: 42 additions & 6 deletions src/translate.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
const translator = require('@vitalets/google-translate-api');
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const translator = require("@vitalets/google-translate-api");

/**
*
* @param {String} string String to translate.
* @param {String} language Language to translate to.
* @param {Boolean} cachingOptions Translation caching options.
* @returns {String}
*/

module.exports = async function translate(string, language) {
if (!string) return console.log("Translator: No Strings Provided!")
module.exports = async function translate(string, language, cachingOptions) {
if (!string) return console.log("Translator: No String Provided!")
if (!language) return console.log("Translator: No Language Provided!")

if (!cachingOptions) return console.log("Translator: No Caching Options Provided!")
if (language === "en") return string; //the string will always be given in english so give the same text back

let hashedString = crypto.createHash("md5").update(string).digest("hex"); //hash the string to use as key

if (cachingOptions.enabled === true) {
let currentCache = fs.existsSync(path.join(process.cwd(), cachingOptions.path, `${language}.json`)) ? JSON.parse(fs.readFileSync(path.join(process.cwd(), cachingOptions.path, `${language}.json`))) : {}; //load the cache file
if (currentCache[hashedString]) {
console.log("Translator: Cache hit for " + string);
return cache[hashedString]; //return cached translation if it exists
}
}

// if either cache is disabled or the cache doesn't exist, translate the string
if (language === "zh") language = "zh-CN";
if (language === "zhcn" || language === "zh-cn") language = "zh-CN";
if (language === "zhtw" || language === "zh-tw") language = "zh-TW";
let translation = await translator.translate(string, { to: language }).catch(e => console.log(e)); //translate the string using google translate
if (!translation) return console.log("Translator: Error occured while translating.");
translation = translation.text;

//save the translation to the cache if caching is enabled
if (cachingOptions.enabled === true) {
//resolve the cache path, create directory if non-existent
let cachePath = path.join(process.cwd(), cachingOptions.path);
if (!fs.existsSync(cachePath)) fs.mkdirSync(cachePath, { recursive: true });

//resolve the cache file, create file if non-existent
let cacheFile = path.join(cachePath, `${language}.json`);
if (!fs.existsSync(cacheFile)) fs.writeFileSync(cacheFile, "{}");

//load the cache file
let cacheToSave = JSON.parse(fs.readFileSync(cacheFile));
cacheToSave[hashedString] = translation; //add the translation to the cache
fs.writeFileSync(cacheFile, JSON.stringify(cacheToSave)); //save the new state of the cache
console.log("Translator: Cache entry created for " + string);
}

let translation = await translator.translate(string, { to: language }).catch(e => console.log(e));
return translation.text;
//return the translation
return translation;
}

0 comments on commit ef876ff

Please sign in to comment.