CVE-2024-29042: Insufficient Information (NVD-CWE-noinfo)
value: Translate is a package that allows users to convert text to different languages on Node.js and the browser. Prior to version 3.0.0, an attacker controlling the second variable of the `translate` function is able to perform a cache poisoning attack. They can change the outcome of translation requests made by subsequent users. The `opt.id` parameter allows the overwriting of the cache key. If an attacker sets the `id` variable to the cache key that would be generated by another user, they can choose the response that user gets served. Version 3.0.0 fixes this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-22T17:15Z
Repository: https://github.com/franciscop/translate
Language: JavaScript
Files changed: README.md, index.min.js, package.json, src/cache.test.js, src/engines/deepl.js, src/engines/google.test.js, src/engines/libre.test.js, src/engines/yandex.test.js, src/index.js, src/translate.test.js, test/index.types.ts, test/mock.js
Commit date: 2024-03-21 11:45:03 +0900
Commit message: Made the usage options much simpler so that there's no chance of writing runtime vulnerabilities

Diff: diff --git a/README.md b/README.md
index b3e42f5..ea021cd 100644
--- a/README.md
+++ b/README.md
@@ -59,27 +59,32 @@ console.log(text);
 
 ## Options
 
 The first parameter is the **string** that you want to translate. Right now only a single string of text is accepted.
 
-The second parameter is the options. It accepts either a `String` of the language to translate **to** or a simple `Object` with these options:
+The second parameter is the options. It accepts either a `String` of the language to translate **to** or a simple `Object` with the keys `to` and `from`. However, in total there are more options, so here is a list of all of them:
 
 - **`to`**: the string of the language to translate to. It can be in any of the two ISO 639 (1 or 2) or the full name in English like `Spanish`. Defaults to **en**.
 - **`from`**: the string of the language to translate to. It can be in any of the two ISO 639 (1 or 2) or the full name in English like `Spanish`. Also defaults to **en**.
-- **`cache`**: a `Number` with the milliseconds that each translation should be cached. Leave it undefined to cache it indefinitely (until a server/browser restart).
-- **`engine`**: a `String` containing the name of the engine to use for translation. Right now it defaults to `google`. Read more [in the engine section](#engines).
-- **`key`**: the API Key for the engine of your choice. Read more [in the engine section](#engines).
-- **`url`**: only available for those engines that you can install on your own server (like Libretranslate), allows you to specify a custom endpoint for the translations. [See this issue](https://github.com/franciscop/translate/issues/26#issuecomment-845038821) for more info.
+- **`cache`** [instance]: a `Number` with the milliseconds that each translation should be cached. Leave it undefined to cache it indefinitely (until a server/browser restart).
+- **`engine`** [instance]: a `String` containing the name of the engine to use for translation. Right now it defaults to `google`. Read more [in the engine section](#engines).
+- **`key`** [instance]: the API Key for the engine of your choice. Read more [in the engine section](#engines).
+- **`url`** [instance]: only available for those engines that you can install on your own server (like Libretranslate), allows you to specify a custom endpoint for the translations. [See this issue](https://github.com/franciscop/translate/issues/26#issuecomment-845038821) for more info.
+
+> The options marked as [instance] can only be set to the root `translate.cache = 1000` or when creating a new instance `const myDeepL = translate.Translate()`
 
 Examples:
 
 ```js
 // Translate from English (default) to Spanish (specified)
 const foo = await translate("Hello world", "es");
 
 // Same as this:
 const bar = await translate("Hello world", { to: "es" });
+
+// INVALID:
+const bar = await translate("Hello world", { to: "es", engine: "google" });
 ```
 
 > On both `to` and `from` defaulting to `en`: while I _am_ Spanish and was quite tempted to set this as one of those, English is the main language of the Internet and the main secondary language for those who have a different native language. This is why most of the translations will happen either to or from English.
 
 ### Default options
@@ -87,13 +92,19 @@ const bar = await translate("Hello world", { to: "es" });
 You can change the default options for anything by calling the root library and the option name:
 
 ```js
 translate.from = "es";
 translate.engine = "deepl";
+await translate("Hola mundo", "ja");
 ```
 
-This can be applied to any of the options enumerated above.
+You can also create a new instance with different default options:
+
+```js
+const myLib = translate.Translate({ engine: 'deepl', from: 'es', ... });
+await myLib("Hola mundo", "ja" );
+```
 
 ## Engines
 
 Several translating engines are available to translate your text:
 
@@ -124,21 +135,40 @@ translate.engine = "deepl";
 translate.key = process.env.TRANSLATE_KEY;
 
 // ... use translate()
 ```
 
-To pass it per-translation, you can add it to your arguments:
+You can create different instances if you want to combine different engines:
 
 ```js
-translate("Hello world", { to: "en", engine: "deepl", key: "YOUR-KEY-HERE" });
+const gTranslate = translate.Translate({
+  engine: "google",
+  key: "YOUR-KEY-HERE",
+});
+const dTranslate = translate.Translate({
+  engine: "deepl",
+  key: "YOUR-KEY-HERE",
+});
+const lTranslate = translate.Translate({
+  engine: "libre",
+  key: "YOUR-KEY-HERE",
+});
 ```
 
 Specifically in Libretranslate, you can also add a `url` parameter if you install it on your own server:
 
 ```js
 translate.url = "https://example.com/";
 translate.key = process.env.TRANSLATE_KEY;
+
+// or
+
+const lTranslate = translate.Translate({
+  engine: "libre",
+  url: "...",
+  key: "YOUR-KEY-HERE",
+});
 ```
 
 ## Promises
 
 Working with Promises and specially with [async/await](https://ponyfoo.com/articles/understanding-javascript-async-await) reduces [Callback Hell](http://callbackhell.com/). To see it in action, first you'll need an `async` function. Then put your `await` calls inside:
diff --git a/index.min.js b/index.min.js
index 94fd6c6..b9af809 100644
--- a/index.min.js
+++ b/index.min.js
@@ -1 +1 @@
-var iso={aar:"aa",abk:"ab",afr:"af",aka:"ak",alb:"sq",amh:"am",ara:"ar",arg:"an",arm:"hy",asm:"as",ava:"av",ave:"ae",aym:"ay",aze:"az",bak:"ba",bam:"bm",baq:"eu",bel:"be",ben:"bn",bih:"bh",bis:"bi",bos:"bs",bre:"br",bul:"bg",bur:"my",cat:"ca",cha:"ch",che:"ce",chi:"zh",chu:"cu",chv:"cv",cor:"kw",cos:"co",cre:"cr",cze:"cs",dan:"da",div:"dv",dut:"nl",dzo:"dz",eng:"en",epo:"eo",est:"et",ewe:"ee",fao:"fo",fij:"fj",fin:"fi",fre:"fr",fry:"fy",ful:"ff",geo:"ka",ger:"de",gla:"gd",gle:"ga",glg:"gl",glv:"gv",gre:"el",grn:"gn",guj:"gu",hat:"ht",hau:"ha",heb:"he",her:"hz",hin:"hi",hmo:"ho",hrv:"hr",hun:"hu",ibo:"ig",ice:"is",ido:"io",iii:"ii",iku:"iu",ile:"ie",ina:"ia",ind:"id",ipk:"ik",ita:"it",jav:"jv",jpn:"ja",kal:"kl",kan:"kn",kas:"ks",kau:"kr",kaz:"kk",khm:"km",kik:"ki",kin:"rw",kir:"ky",kom:"kv",kon:"kg",kor:"ko",kua:"kj",kur:"ku",lao:"lo",lat:"la",lav:"lv",lim:"li",lin:"ln",lit:"lt",ltz:"lb",lub:"lu",lug:"lg",mac:"mk",mah:"mh",mal:"ml",mao:"mi",mar:"mr",may:"ms",mlg:"mg",mlt:"mt",mon:"mn",nau:"na",nav:"nv",nbl:"nr",nde:"nd",ndo:"ng",nep:"ne",nno:"nn",nob:"nb",nor:"no",nya:"ny",oci:"oc",oji:"oj",ori:"or",orm:"om",oss:"os",pan:"pa",per:"fa",pli:"pi",pol:"pl",por:"pt",pus:"ps",que:"qu",roh:"rm",rum:"ro",run:"rn",rus:"ru",sag:"sg",san:"sa",sin:"si",slo:"sk",slv:"sl",sme:"se",smo:"sm",sna:"sn",snd:"sd",som:"so",sot:"st",spa:"es",srd:"sc",srp:"sr",ssw:"ss",sun:"su",swa:"sw",swe:"sv",tah:"ty",tam:"ta",tat:"tt",tel:"te",tgk:"tg",tgl:"tl",tha:"th",tib:"bo",tir:"ti",ton:"to",tsn:"tn",tso:"ts",tuk:"tk",tur:"tr",twi:"tw",uig:"ug",ukr:"uk",urd:"ur",uzb:"uz",ven:"ve",vie:"vi",vol:"vo",wel:"cy",wln:"wa",wol:"wo",xho:"xh",yid:"yi",yor:"yo",zha:"za",zul:"zu"},names={afar:"aa",abkhazian:"ab",afrikaans:"af",akan:"ak",albanian:"sq",amharic:"am",arabic:"ar",aragonese:"an",armenian:"hy",assamese:"as",avaric:"av",avestan:"ae",aymara:"ay",azerbaijani:"az",bashkir:"ba",bambara:"bm",basque:"eu",belarusian:"be",bengali:"bn","bihari languages":"bh",bislama:"bi",tibetan:"bo",bosnian:"bs",breton:"br",bulgarian:"bg",burmese:"my",catalan:"ca",valencian:"ca",czech:"cs",chamorro:"ch",chechen:"ce",chinese:"zh","church slavic":"cu","old slavonic":"cu","church slavonic":"cu","old bulgarian":"cu","old church slavonic":"cu",chuvash:"cv",cornish:"kw",corsican:"co",cree:"cr",welsh:"cy",danish:"da",german:"de",divehi:"dv",dhivehi:"dv",maldivian:"dv",dutch:"nl",flemish:"nl",dzongkha:"dz",greek:"el",english:"en",esperanto:"eo",estonian:"et",ewe:"ee",faroese:"fo",persian:"fa",fijian:"fj",finnish:"fi",french:"fr","western frisian":"fy",fulah:"ff",georgian:"ka",gaelic:"gd","scottish gaelic":"gd",irish:"ga",galician:"gl",manx:"gv",guarani:"gn",gujarati:"gu",haitian:"ht","haitian creole":"ht",hausa:"ha",hebrew:"he",herero:"hz",hindi:"hi","hiri motu":"ho",croatian:"hr",hungarian:"hu",igbo:"ig",icelandic:"is",ido:"io","sichuan yi":"ii",nuosu:"ii",inuktitut:"iu",interlingue:"ie",occidental:"ie",interlingua:"ia",indonesian:"id",inupiaq:"ik",italian:"it",javanese:"jv",japanese:"ja",kalaallisut:"kl",greenlandic:"kl",kannada:"kn",kashmiri:"ks",kanuri:"kr",kazakh:"kk","central khmer":"km",kikuyu:"ki",gikuyu:"ki",kinyarwanda:"rw",kirghiz:"ky",kyrgyz:"ky",komi:"kv",kongo:"kg",korean:"ko",kuanyama:"kj",kwanyama:"kj",kurdish:"ku",lao:"lo",latin:"la",latvian:"lv",limburgan:"li",limburger:"li",limburgish:"li",lingala:"ln",lithuanian:"lt",luxembourgish:"lb",letzeburgesch:"lb","luba-katanga":"lu",ganda:"lg",macedonian:"mk",marshallese:"mh",malayalam:"ml",maori:"mi",marathi:"mr",malay:"ms",malagasy:"mg",maltese:"mt",mongolian:"mn",nauru:"na",navajo:"nv",navaho:"nv","ndebele, south":"nr","south ndebele":"nr","ndebele, north":"nd","north ndebele":"nd",ndonga:"ng",nepali:"ne","norwegian nynorsk":"nn","nynorsk, norwegian":"nn","norwegian bokmål":"nb","bokmål, norwegian":"nb",norwegian:"no",chichewa:"ny",chewa:"ny",nyanja:"ny",occitan:"oc",ojibwa:"oj",oriya:"or",oromo:"om",ossetian:"os",ossetic:"os",panjabi:"pa",punjabi:"pa",pali:"pi",polish:"pl",portuguese:"pt",pushto:"ps",pashto:"ps",quechua:"qu",romansh:"rm",romanian:"ro",moldavian:"ro",moldovan:"ro",rundi:"rn",russian:"ru",sango:"sg",sanskrit:"sa",sinhala:"si",sinhalese:"si",slovak:"sk",slovenian:"sl","northern sami":"se",samoan:"sm",shona:"sn",sindhi:"sd",somali:"so","sotho, southern":"st",spanish:"es",castilian:"es",sardinian:"sc",serbian:"sr",swati:"ss",sundanese:"su",swahili:"sw",swedish:"sv",tahitian:"ty",tamil:"ta",tatar:"tt",telugu:"te",tajik:"tg",tagalog:"tl",thai:"th",tigrinya:"ti",tonga:"to",tswana:"tn",tsonga:"ts",turkmen:"tk",turkish:"tr",twi:"tw",uighur:"ug",uyghur:"ug",ukrainian:"uk",urdu:"ur",uzbek:"uz",venda:"ve",vietnamese:"vi","volapük":"vo",walloon:"wa",wolof:"wo",xhosa:"xh",yiddish:"yi",yoruba:"yo",zhuang:"za",chuang:"za",zulu:"zu"};const isoKeys=Object.values(iso).sort();var languages=e=>{if("string"!=typeof e)throw new Error('The "language" must be a string, received '+typeof e);if(e.length>100)throw new Error(`The "language" is too long at ${e.length} characters`);if(e=e.toLowerCase(),e=names[e]||iso[e]||e,!isoKeys.includes(e))throw new Error(`The language "${e}" is not part of the ISO 639-1`);return e};function Cache(){var e=Object.create(null);function a(a){delete e[a]}this.set=function(n,i,r){if(void 0!==r&&("number"!=typeof r||isNaN(r)||r<=0))throw new Error("Cache timeout must be a positive number");var t=e[n];t&&clearTimeout(t.timeout);var o={value:i,expire:r+Date.now()};return isNaN(o.expire)||(o.timeout=setTimeout((()=>a(n)),r)),e[n]=o,i},this.del=function(n){var i=!0,r=e[n];return r?(clearTimeout(r.timeout),!isNaN(r.expire)&&r.expire<Date.now()&&(i=!1)):i=!1,i&&a(n),i},this.clear=function(){for(var a in e)clearTimeout(e[a].timeout);e=Object.create(null)},this.get=function(a){var n=e[a];if(void 0!==n){if(isNaN(n.expire)||n.expire>=Date.now())return n.value;delete e[a]}return null}}const exp$1=new Cache;exp$1.Cache=Cache;const base="https://translate.googleapis.com/translate_a/single";var google={fetch:({key:e,from:a,to:n,text:i})=>[`${base}?client=gtx&sl=${a}&tl=${n}&dt=t&q=${encodeURI(i)}`],parse:e=>e.json().then((e=>{if(!(e=e&&e[0]&&e[0][0]&&e[0].map((e=>e[0])).join("")))throw new Error("Translation not found");return e}))},yandex={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://translate.yandex.net/api/v1.5/tr.json/translate?key=${e}&lang=${a}-${n}&text=${encodeURIComponent(i)}`,{method:"POST",body:""}],parse:e=>e.json().then((e=>{if(200!==e.code)throw new Error(e.message);return e.text[0]}))};const libreUrl="https://libretranslate.com/translate";var libre={needkey:!1,fetch:({url:e=libreUrl,key:a,from:n,to:i,text:r})=>[e,{method:"POST",body:JSON.stringify({q:r,source:n,target:i,api_key:a}),headers:{"Content-Type":"application/json"}}],parse:e=>e.json().then((e=>{if(!e)throw new Error("No response found");if(e.error)throw new Error(e.error);if(!e.translatedText)throw new Error("No response found");return e.translatedText}))},deepl={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://api${/:fx$/.test(e)?"-free":""}.deepl.com/v2/translate?auth_key=${e}&source_lang=${a}&target_lang=${n}&text=${i=encodeURIComponent(i)}`,{method:"POST",body:""}],parse:async e=>{if(!e.ok){if(403===e.status)throw new Error("Auth Error, please review the key for DeepL");throw new Error(`Error ${e.status}`)}return e.json().then((e=>e.translations[0].text))}},engines={google:google,yandex:yandex,libre:libre,deepl:deepl};const Translate=function(e={}){if(!(this instanceof Translate))return new Translate(e);const a={from:"en",to:"en",cache:void 0,languages:languages,engines:engines,engine:"google",keys:{}},n=async(e,a={})=>{"string"==typeof a&&(a={to:a}),a.text=e,a.from=languages(a.from||n.from),a.to=languages(a.to||n.to),a.cache=a.cache||n.cache,a.engines=a.engines||{},a.engine=a.engine||n.engine,a.url=a.url||n.url,a.id=a.id||`${a.url}:${a.from}:${a.to}:${a.engine}:${a.text}`,a.keys=a.keys||n.keys||{};for(let e in n.keys)a.keys[e]=a.keys[e]||n.keys[e];a.key=a.key||n.key||a.keys[a.engine];const i=a.engines[a.engine]||n.engines[a.engine],r=exp$1.get(a.id);if(r)return Promise.resolve(r);if(a.to===a.from)return Promise.resolve(a.text);if(i.needkey&&!a.key)throw new Error(`The engine "${a.engine}" needs a key, please provide it`);const t=i.fetch(a);return fetch(...t).then(i.parse).then((e=>exp$1.set(a.id,e,a.cache)))};for(let i in a)n[i]=void 0===e[i]?a[i]:e[i];return n},exp=new Translate;exp.Translate=Translate;export{exp as default};
\ No newline at end of file
+function Cache(){var e=Object.create(null);function a(a){delete e[a]}this.set=function(n,i,r){if(void 0!==r&&("number"!=typeof r||isNaN(r)||r<=0))throw new Error("Cache timeout must be a positive number");var t=e[n];t&&clearTimeout(t.timeout);var o={value:i,expire:r+Date.now()};return isNaN(o.expire)||(o.timeout=setTimeout((()=>a(n)),r)),e[n]=o,i},this.del=function(n){var i=!0,r=e[n];return r?(clearTimeout(r.timeout),!isNaN(r.expire)&&r.expire<Date.now()&&(i=!1)):i=!1,i&&a(n),i},this.clear=function(){for(var a in e)clearTimeout(e[a].timeout);e=Object.create(null)},this.get=function(a){var n=e[a];if(void 0!==n){if(isNaN(n.expire)||n.expire>=Date.now())return n.value;delete e[a]}return null}}const exp$1=new Cache;exp$1.Cache=Cache;const base="https://translate.googleapis.com/translate_a/single";var google={fetch:({key:e,from:a,to:n,text:i})=>[`${base}?client=gtx&sl=${a}&tl=${n}&dt=t&q=${encodeURI(i)}`],parse:e=>e.json().then((e=>{if(!(e=e&&e[0]&&e[0][0]&&e[0].map((e=>e[0])).join("")))throw new Error("Translation not found");return e}))},yandex={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://translate.yandex.net/api/v1.5/tr.json/translate?key=${e}&lang=${a}-${n}&text=${encodeURIComponent(i)}`,{method:"POST",body:""}],parse:e=>e.json().then((e=>{if(200!==e.code)throw new Error(e.message);return e.text[0]}))};const libreUrl="https://libretranslate.com/translate";var libre={needkey:!1,fetch:({url:e=libreUrl,key:a,from:n,to:i,text:r})=>[e,{method:"POST",body:JSON.stringify({q:r,source:n,target:i,api_key:a}),headers:{"Content-Type":"application/json"}}],parse:e=>e.json().then((e=>{if(!e)throw new Error("No response found");if(e.error)throw new Error(e.error);if(!e.translatedText)throw new Error("No response found");return e.translatedText}))},deepl={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://api${e.endsWith(":fx")?"-free":""}.deepl.com/v2/translate?auth_key=${e}&source_lang=${a}&target_lang=${n}&text=${i=encodeURIComponent(i)}`,{method:"POST",body:""}],parse:async e=>{if(!e.ok){if(403===e.status)throw new Error("Auth Error, please review the key for DeepL");throw new Error(`Error ${e.status}`)}return e.json().then((e=>e.translations[0].text))}},engines={google:google,yandex:yandex,libre:libre,deepl:deepl},iso={aar:"aa",abk:"ab",afr:"af",aka:"ak",alb:"sq",amh:"am",ara:"ar",arg:"an",arm:"hy",asm:"as",ava:"av",ave:"ae",aym:"ay",aze:"az",bak:"ba",bam:"bm",baq:"eu",bel:"be",ben:"bn",bih:"bh",bis:"bi",bos:"bs",bre:"br",bul:"bg",bur:"my",cat:"ca",cha:"ch",che:"ce",chi:"zh",chu:"cu",chv:"cv",cor:"kw",cos:"co",cre:"cr",cze:"cs",dan:"da",div:"dv",dut:"nl",dzo:"dz",eng:"en",epo:"eo",est:"et",ewe:"ee",fao:"fo",fij:"fj",fin:"fi",fre:"fr",fry:"fy",ful:"ff",geo:"ka",ger:"de",gla:"gd",gle:"ga",glg:"gl",glv:"gv",gre:"el",grn:"gn",guj:"gu",hat:"ht",hau:"ha",heb:"he",her:"hz",hin:"hi",hmo:"ho",hrv:"hr",hun:"hu",ibo:"ig",ice:"is",ido:"io",iii:"ii",iku:"iu",ile:"ie",ina:"ia",ind:"id",ipk:"ik",ita:"it",jav:"jv",jpn:"ja",kal:"kl",kan:"kn",kas:"ks",kau:"kr",kaz:"kk",khm:"km",kik:"ki",kin:"rw",kir:"ky",kom:"kv",kon:"kg",kor:"ko",kua:"kj",kur:"ku",lao:"lo",lat:"la",lav:"lv",lim:"li",lin:"ln",lit:"lt",ltz:"lb",lub:"lu",lug:"lg",mac:"mk",mah:"mh",mal:"ml",mao:"mi",mar:"mr",may:"ms",mlg:"mg",mlt:"mt",mon:"mn",nau:"na",nav:"nv",nbl:"nr",nde:"nd",ndo:"ng",nep:"ne",nno:"nn",nob:"nb",nor:"no",nya:"ny",oci:"oc",oji:"oj",ori:"or",orm:"om",oss:"os",pan:"pa",per:"fa",pli:"pi",pol:"pl",por:"pt",pus:"ps",que:"qu",roh:"rm",rum:"ro",run:"rn",rus:"ru",sag:"sg",san:"sa",sin:"si",slo:"sk",slv:"sl",sme:"se",smo:"sm",sna:"sn",snd:"sd",som:"so",sot:"st",spa:"es",srd:"sc",srp:"sr",ssw:"ss",sun:"su",swa:"sw",swe:"sv",tah:"ty",tam:"ta",tat:"tt",tel:"te",tgk:"tg",tgl:"tl",tha:"th",tib:"bo",tir:"ti",ton:"to",tsn:"tn",tso:"ts",tuk:"tk",tur:"tr",twi:"tw",uig:"ug",ukr:"uk",urd:"ur",uzb:"uz",ven:"ve",vie:"vi",vol:"vo",wel:"cy",wln:"wa",wol:"wo",xho:"xh",yid:"yi",yor:"yo",zha:"za",zul:"zu"},names={afar:"aa",abkhazian:"ab",afrikaans:"af",akan:"ak",albanian:"sq",amharic:"am",arabic:"ar",aragonese:"an",armenian:"hy",assamese:"as",avaric:"av",avestan:"ae",aymara:"ay",azerbaijani:"az",bashkir:"ba",bambara:"bm",basque:"eu",belarusian:"be",bengali:"bn","bihari languages":"bh",bislama:"bi",tibetan:"bo",bosnian:"bs",breton:"br",bulgarian:"bg",burmese:"my",catalan:"ca",valencian:"ca",czech:"cs",chamorro:"ch",chechen:"ce",chinese:"zh","church slavic":"cu","old slavonic":"cu","church slavonic":"cu","old bulgarian":"cu","old church slavonic":"cu",chuvash:"cv",cornish:"kw",corsican:"co",cree:"cr",welsh:"cy",danish:"da",german:"de",divehi:"dv",dhivehi:"dv",maldivian:"dv",dutch:"nl",flemish:"nl",dzongkha:"dz",greek:"el",english:"en",esperanto:"eo",estonian:"et",ewe:"ee",faroese:"fo",persian:"fa",fijian:"fj",finnish:"fi",french:"fr","western frisian":"fy",fulah:"ff",georgian:"ka",gaelic:"gd","scottish gaelic":"gd",irish:"ga",galician:"gl",manx:"gv",guarani:"gn",gujarati:"gu",haitian:"ht","haitian creole":"ht",hausa:"ha",hebrew:"he",herero:"hz",hindi:"hi","hiri motu":"ho",croatian:"hr",hungarian:"hu",igbo:"ig",icelandic:"is",ido:"io","sichuan yi":"ii",nuosu:"ii",inuktitut:"iu",interlingue:"ie",occidental:"ie",interlingua:"ia",indonesian:"id",inupiaq:"ik",italian:"it",javanese:"jv",japanese:"ja",kalaallisut:"kl",greenlandic:"kl",kannada:"kn",kashmiri:"ks",kanuri:"kr",kazakh:"kk","central khmer":"km",kikuyu:"ki",gikuyu:"ki",kinyarwanda:"rw",kirghiz:"ky",kyrgyz:"ky",komi:"kv",kongo:"kg",korean:"ko",kuanyama:"kj",kwanyama:"kj",kurdish:"ku",lao:"lo",latin:"la",latvian:"lv",limburgan:"li",limburger:"li",limburgish:"li",lingala:"ln",lithuanian:"lt",luxembourgish:"lb",letzeburgesch:"lb","luba-katanga":"lu",ganda:"lg",macedonian:"mk",marshallese:"mh",malayalam:"ml",maori:"mi",marathi:"mr",malay:"ms",malagasy:"mg",maltese:"mt",mongolian:"mn",nauru:"na",navajo:"nv",navaho:"nv","ndebele, south":"nr","south ndebele":"nr","ndebele, north":"nd","north ndebele":"nd",ndonga:"ng",nepali:"ne","norwegian nynorsk":"nn","nynorsk, norwegian":"nn","norwegian bokmål":"nb","bokmål, norwegian":"nb",norwegian:"no",chichewa:"ny",chewa:"ny",nyanja:"ny",occitan:"oc",ojibwa:"oj",oriya:"or",oromo:"om",ossetian:"os",ossetic:"os",panjabi:"pa",punjabi:"pa",pali:"pi",polish:"pl",portuguese:"pt",pushto:"ps",pashto:"ps",quechua:"qu",romansh:"rm",romanian:"ro",moldavian:"ro",moldovan:"ro",rundi:"rn",russian:"ru",sango:"sg",sanskrit:"sa",sinhala:"si",sinhalese:"si",slovak:"sk",slovenian:"sl","northern sami":"se",samoan:"sm",shona:"sn",sindhi:"sd",somali:"so","sotho, southern":"st",spanish:"es",castilian:"es",sardinian:"sc",serbian:"sr",swati:"ss",sundanese:"su",swahili:"sw",swedish:"sv",tahitian:"ty",tamil:"ta",tatar:"tt",telugu:"te",tajik:"tg",tagalog:"tl",thai:"th",tigrinya:"ti",tonga:"to",tswana:"tn",tsonga:"ts",turkmen:"tk",turkish:"tr",twi:"tw",uighur:"ug",uyghur:"ug",ukrainian:"uk",urdu:"ur",uzbek:"uz",venda:"ve",vietnamese:"vi","volapük":"vo",walloon:"wa",wolof:"wo",xhosa:"xh",yiddish:"yi",yoruba:"yo",zhuang:"za",chuang:"za",zulu:"zu"};const isoKeys=Object.values(iso).sort();var languages=e=>{if("string"!=typeof e)throw new Error('The "language" must be a string, received '+typeof e);if(e.length>100)throw new Error(`The "language" is too long at ${e.length} characters`);if(e=e.toLowerCase(),e=names[e]||iso[e]||e,!isoKeys.includes(e))throw new Error(`The language "${e}" is not part of the ISO 639-1`);return e};const Translate=function(e={}){if(!(this instanceof Translate))return new Translate(e);const a={from:"en",to:"en",cache:void 0,engine:"google",key:void 0,url:void 0,languages:languages,engines:engines,keys:{}},n=async(e,a={})=>{"string"==typeof a&&(a={to:a});const i=Object.keys(a).find((e=>"from"!==e&&"to"!==e));if(i)throw new Error(`Invalid option with the name '${i}'`);a.text=e,a.from=languages(a.from||n.from),a.to=languages(a.to||n.to),a.cache=n.cache,a.engine=n.engine,a.url=n.url,a.id=`${a.url}:${a.from}:${a.to}:${a.engine}:${a.text}`,a.keys=n.keys||{};for(let e in n.keys)a.keys[e]=a.keys[e]||n.keys[e];a.key=a.key||n.key||a.keys[a.engine];const r=n.engines[a.engine],t=exp$1.get(a.id);if(t)return Promise.resolve(t);if(a.to===a.from)return Promise.resolve(a.text);if(r.needkey&&!a.key)throw new Error(`The engine "${a.engine}" needs a key, please provide it`);const o=r.fetch(a);return fetch(...o).then(r.parse).then((e=>exp$1.set(a.id,e,a.cache)))};for(let i in a)n[i]=void 0===e[i]?a[i]:e[i];return n},exp=new Translate;exp.Translate=Translate;export{exp as default};
\ No newline at end of file
diff --git a/package.json b/package.json
index 441848b..df7a6e0 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,17 @@
 {
   "name": "translate",
   "version": "2.0.2",
   "description": "Translate text to different languages on node.js and the browser",
+  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
   "homepage": "https://github.com/franciscop/translate#readme",
   "repository": "https://github.com/franciscop/translate.git",
   "bugs": "https://github.com/franciscop/translate/issues",
   "funding": "https://www.paypal.me/franciscopresencia/19",
-  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
   "license": "MIT",
   "scripts": {
-    "start": "npm run watch # Start ~= Start dev",
+    "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
     "build": "rollup src/index.js --name translate --output.format esm | terser --compress --mangle -o index.min.js",
     "size": "gzip -c index.min.js | wc -c && echo 'bytes' # Only for Unix",
     "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --collectCoverageFrom=src/**/*.js && npx check-dts",
     "watch": "nodemon --exec \"npm run build && npm test && npm run gzip\" --watch src --watch test --watch webpack.config.js --watch package.json"
   },
diff --git a/src/cache.test.js b/src/cache.test.js
index 2aeca3e..43ffcd3 100644
--- a/src/cache.test.js
+++ b/src/cache.test.js
@@ -33,16 +33,17 @@ describe("cache", () => {
     expect(after - mid).toBeLessThan(10);
     expect(after - mid).toBeGreaterThanOrEqual(0);
   });
 
   it("removes cache after the time is out", async () => {
+    const t = translate.Translate({ cache: 1000 });
     const before = new Date();
-    await translate("Is this also cached?", { to: "es", cache: 1000 });
+    await t("Is this also cached?", { to: "es" });
     const mid = new Date();
     await delay(1100);
     mock(/googleapis.*tl=es/, [[["Hola mundo"]]]);
-    await translate("Is this also cached?", { to: "es" });
+    await t("Is this also cached?", { to: "es" });
     const after = new Date();
     expect(mid - before).toBeLessThan(10000);
     expect(mid - before).toBeGreaterThan(100);
     expect(after - mid).toBeLessThan(10000);
     expect(after - mid).toBeGreaterThan(100);
diff --git a/src/engines/deepl.js b/src/engines/deepl.js
index a5c343a..48465f0 100644
--- a/src/engines/deepl.js
+++ b/src/engines/deepl.js
@@ -1,9 +1,9 @@
 export default {
   needkey: true,
   fetch: ({ key, from, to, text }) => {
-    const suffix = /:fx$/.test(key) ? "-free" : "";
+    const suffix = key.endsWith(":fx") ? "-free" : "";
     text = encodeURIComponent(text);
     return [
       `https://api${suffix}.deepl.com/v2/translate?auth_key=${key}&source_lang=${from}&target_lang=${to}&text=${text}`,
       { method: "POST", body: "" },
     ];
diff --git a/src/engines/google.test.js b/src/engines/google.test.js
index 79d8089..a89fec7 100644
--- a/src/engines/google.test.js
+++ b/src/engines/google.test.js
@@ -1,9 +1,12 @@
 import "dotenv/config";
-import translate from "../../src";
+
+import t from "../../src";
 import mock from "../../test/mock.js";
 
+const translate = t.Translate({ engine: "google" });
+
 describe("google mocked responses", () => {
   afterEach(() => mock.end());
 
   it("can translate a simple string", async () => {
     mock.google("Hola mundo");
@@ -37,21 +40,21 @@ describe("google full requests", () => {
   it("has an engine", () => {
     expect(translate.engines.google).toBeDefined();
   });
 
   it("calls Google to translate to Japanese", async () => {
-    const opts = { to: "ja", engine: "google" };
+    const opts = { to: "ja" };
     expect(await translate("Hello world", opts)).toBe("こんにちは世界");
   });
 
   it("calls Google to translate to Spanish", async () => {
-    const opts = { to: "es", engine: "google" };
+    const opts = { to: "es" };
     expect(await translate("Hello world", opts)).toMatch(/Hola mundo/i);
   });
 
   it("works with punctuation", async () => {
-    const opts = { to: "pt", engine: "google" };
+    const opts = { to: "pt" };
     const text = await translate(
       "What do you call a pig that knows karate? A pork chop!",
       opts
     );
     expect(text).toBe(
diff --git a/src/engines/libre.test.js b/src/engines/libre.test.js
index ea5c569..8894875 100644
--- a/src/engines/libre.test.js
+++ b/src/engines/libre.test.js
@@ -1,48 +1,50 @@
 import "dotenv/config";
-import translate from "../../src";
+
+import t from "../../src";
 import mock from "../../test/mock";
 
+const translate = t.Translate({ engine: "libre" });
 translate.keys.libre = process.env.LIBRE_KEY || "xxx";
 
 describe("Libre mocked requests", () => {
   beforeEach(() => mock.end());
   afterEach(() => mock.end());
 
   it("works with libretranslate", async () => {
     mock.libre("Hola mundo");
-    const text = await translate("Hello world", { to: "es", engine: "libre" });
+    const text = await translate("Hello world", { to: "es" });
     expect(text).toMatch(/Hola mundo/i);
   });
 
   it("will throw with a wrong language", async () => {
-    const opts = { to: "adgdfnj", engine: "libre" };
+    const opts = { to: "adgdfnj" };
     await expect(translate("Hello world", opts)).rejects.toMatchObject({
       message: `The language "adgdfnj" is not part of the ISO 639-1`,
     });
   });
 
   it("will throw with an empty result", async () => {
     mock.libre("");
-    const opts = { to: "es", engine: "libre" };
+    const opts = { to: "es" };
     await expect(translate("What's going on?", opts)).rejects.toMatchObject({
       message: "No response found",
     });
   });
 
   it("requires an API key", async () => {
-    const opts = { to: "es", engine: "libre" };
+    const opts = { to: "es" };
     await expect(translate("What's going on?", opts)).rejects.toMatchObject({
       message: "Visit https://portal.libretranslate.com to get an API key",
     });
   });
 
   it("well the domain is wrong", async () => {
     mock(/example\.*/, new Error("no domain"), { throws: true });
 
     translate.url = "https://example.com/";
-    const opts = { to: "es", engine: "libre" };
+    const opts = { to: "es" };
     await expect(translate("Hello world", opts)).rejects.toMatchObject({
       message: "no domain",
     });
   });
 });
@@ -57,27 +59,27 @@ describe("libre full requests", () => {
   if (!process.env.LIBRE_KEY || process.env.LIBRE_KEY === "xxx") {
     return console.log("Libre [PAID] tests skipped");
   }
 
   it("calls Libre to translate to Japanese", async () => {
-    const opts = { to: "ja", engine: "libre" };
+    const opts = { to: "ja" };
     expect(await translate("Hello world", opts)).toBe("ハローワールド");
   });
 
   it("calls Libre to translate to Spanish", async () => {
-    const opts = { to: "es", engine: "libre" };
+    const opts = { to: "es" };
     expect(await translate("Hello world", opts)).toBe("Hola mundo");
   });
 
   it("requires a valid key", async () => {
-    const opts = { to: "ru", engine: "libre", key: "abc" };
+    const opts = { to: "ru", key: "abc" };
     await expect(translate("Hello world", opts)).rejects.toThrow();
   });
 
   it("can set a custom URL", async () => {
     translate.url = "https://example.com/";
-    const opts = { to: "es", engine: "libre" };
+    const opts = { to: "es" };
     const text = await translate("libre custom url", opts);
     delete translate.url;
     expect(text).toBe("Url personalizada de libre");
   });
 });
diff --git a/src/engines/yandex.test.js b/src/engines/yandex.test.js
index 059012e..8fd3c4d 100644
--- a/src/engines/yandex.test.js
+++ b/src/engines/yandex.test.js
@@ -1,9 +1,11 @@
 import "dotenv/config";
-import translate from "../../src";
+
+import t from "../../src";
 import mock from "../../test/mock.js";
 
+const translate = t.Translate({ engine: "yandex" });
 translate.keys.yandex = process.env.YANDEX_KEY || "xxx";
 
 describe("yandex mocked requests", () => {
   beforeEach(() => {
     mock(/yandex.*error/, { code: 500, message: "it fails" });
@@ -16,24 +18,21 @@ describe("yandex mocked requests", () => {
 
   afterEach(() => mock.end());
 
   it("works with a simple request", async () => {
     mock.yandex("Hola de Yandex");
-    const spanish = await translate("Hello from Yandex", {
-      to: "es",
-      engine: "yandex",
-    });
+    const spanish = await translate("Hello from Yandex", { to: "es" });
     expect(spanish).toMatch(/Hola de Yandex/i);
   });
 
   it("can handle errors from the API", async () => {
-    const prom = translate("error", { to: "es", engine: "yandex" });
+    const prom = translate("error", { to: "es" });
     await expect(prom).rejects.toHaveProperty("message", "it fails");
   });
 
   it("can handle errors thrown by fetch()", async () => {
-    const prom = translate("throw", { to: "es", engine: "yandex" });
+    const prom = translate("throw", { to: "es" });
     await expect(prom).rejects.toHaveProperty("message", "also fails harder");
   });
 });
 
 describe("yandex full requests", () => {
@@ -44,19 +43,19 @@ describe("yandex full requests", () => {
   if (!process.env.YANDEX_KEY || process.env.YANDEX_KEY === "xxx") {
     return console.log("Yandex [PAID] tests skipped");
   }
 
   it("calls Yandex to translate to Japanese", async () => {
-    const opts = { to: "ja", engine: "yandex" };
+    const opts = { to: "ja" };
     expect(await translate("Hello world", opts)).toBe("こんにちは世界");
   });
 
   it("calls Yandex to translate to Spanish", async () => {
-    const opts = { to: "es", engine: "yandex" };
+    const opts = { to: "es" };
     expect(await translate("Hello world", opts)).toBe("Hola mundo");
   });
 
   it("requires a valid key", async () => {
-    const opts = { to: "ru", engine: "yandex", key: "abc" };
+    const opts = { to: "ru", key: "abc" };
     await expect(translate("Hello world", opts)).rejects.toThrow();
   });
 });
diff --git a/src/index.js b/src/index.js
index 4a80c20..5070b27 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,15 +1,13 @@
 // translate.js
 // Translate text into different languages;
 
-// Load a language parser to allow for more flexibility in the language choice
-import languages from "./languages/index.js";
-
 // Cache the different translations to avoid resending requests
 import cache from "./cache.js";
-
 import engines from "./engines/index.js";
+// Load a language parser to allow for more flexibility in the language choice
+import languages from "./languages/index.js";
 
 // Main function
 const Translate = function (options = {}) {
   if (!(this instanceof Translate)) {
     return new Translate(options);
@@ -17,39 +15,44 @@ const Translate = function (options = {}) {
 
   const defaults = {
     from: "en",
     to: "en",
     cache: undefined,
+    engine: "google",
+    key: undefined,
+    url: undefined,
     languages: languages,
     engines: engines,
-    engine: "google",
     keys: {},
   };
 
   const translate = async (text, opts = {}) => {
     // Load all of the appropriate options (verbose but fast)
     // Note: not all of those *should* be documented since some are internal only
     if (typeof opts === "string") opts = { to: opts };
+    const invalidKey = Object.keys(opts).find(
+      (k) => k !== "from" && k !== "to"
+    );
+    if (invalidKey) {
+      throw new Error(`Invalid option with the name '${invalidKey}'`);
+    }
     opts.text = text;
     opts.from = languages(opts.from || translate.from);
     opts.to = languages(opts.to || translate.to);
-    opts.cache = opts.cache || translate.cache;
-    opts.engines = opts.engines || {};
-    opts.engine = opts.engine || translate.engine;
-    opts.url = opts.url || translate.url;
-    opts.id =
-      opts.id ||
-      `${opts.url}:${opts.from}:${opts.to}:${opts.engine}:${opts.text}`;
-    opts.keys = opts.keys || translate.keys || {};
+    opts.cache = translate.cache;
+    opts.engine = translate.engine;
+    opts.url = translate.url;
+    opts.id = `${opts.url}:${opts.from}:${opts.to}:${opts.engine}:${opts.text}`;
+    opts.keys = translate.keys || {};
     for (let name in translate.keys) {
       // The options has stronger preference than the global value
       opts.keys[name] = opts.keys[name] || translate.keys[name];
     }
     opts.key = opts.key || translate.key || opts.keys[opts.engine];
 
     // Use the desired engine
-    const engine = opts.engines[opts.engine] || translate.engines[opts.engine];
+    const engine = translate.engines[opts.engine];
 
     // If it is cached return ASAP
     const cached = cache.get(opts.id);
     if (cached) return Promise.resolve(cached);
 
diff --git a/src/translate.test.js b/src/translate.test.js
index af73334..0b343a8 100644
--- a/src/translate.test.js
+++ b/src/translate.test.js
@@ -1,10 +1,12 @@
 import "dotenv/config";
-import translate from "./";
-import mock from "../test/mock";
+
 import fs from "fs";
 
+import mock from "../test/mock";
+import translate from "./";
+
 describe("translate.js", () => {
   afterEach(() => mock.end());
 
   it("loads", () => {
     expect(translate).toBeDefined();
@@ -45,16 +47,21 @@ describe("translate.js", () => {
       to: "JapAnesE",
     });
     expect(jp).toBe("こんにちは世界");
   });
 
-  it("requires the key", async () => {
+  it("does not accept other keys", async () => {
     await expect(() =>
-      translate("hello", { engine: "yandex", key: false, to: "es" })
-    ).rejects.toMatchObject({
-      message: 'The engine "yandex" needs a key, please provide it',
-    });
+      translate("hello", { to: "es", blabla: "blu" })
+    ).rejects.toThrow("Invalid option with the name 'blabla'");
+  });
+
+  it("requires the key", async () => {
+    const yandex = translate.Translate({ engine: "yandex", key: false });
+    await expect(() => yandex("hello", "es")).rejects.toThrow(
+      'The engine "yandex" needs a key, please provide it'
+    );
   });
 });
 
 describe("Independent", () => {
   it("has independent instances", () => {
@@ -70,13 +77,16 @@ describe("Independent", () => {
     expect(inst.from).toBe("en");
     inst = new translate.Translate();
     expect(inst.from).toBe("en");
   });
 
-  it.skip("fixed the bug #5", async () => {
-    // translate.keys = { google: 'abc' };
-    const options = { to: "ja", keys: { yandex: "def" }, engine: "google" };
-
-    // This will wrongly ignore the key for "google"
-    expect(await translate("Hello world", options)).toBe("こんにちは世界");
+  it("cannot poison the cache", async () => {
+    const options = {
+      id: "undefined:en:nl:google:I love you",
+      from: "en",
+      to: "ja",
+    };
+    await expect(() => translate("hello", options)).rejects.toThrow(
+      "Invalid option with the name 'id'"
+    );
   });
 });
diff --git a/test/index.types.ts b/test/index.types.ts
index 84324af..2729886 100644
--- a/test/index.types.ts
+++ b/test/index.types.ts
@@ -1,6 +1,6 @@
-import translate from "../";
+import translate from "..";
 
 // Don't even need to run it to test the types!
 async function tests() {
   translate("hello world", "es");
   // translate("hello world", { abc: "def" });
diff --git a/test/mock.js b/test/mock.js
index be267f3..90c52ca 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -21,11 +21,11 @@ mock.google = (content, opts) => {
 mock.libre = (content, opts) => {
   mock(/libretranslate\.com\//, { code: 200, translatedText: content }, opts);
 };
 
 mock.yandex = (content, opts) => {
-  mock(/yandex.*&lang=[a-z]*\-es/, { code: 200, text: [content] }, opts);
+  mock(/yandex.*&lang=[a-z]*-es/, { code: 200, text: [content] }, opts);
 };
 
 mock.end = () => {
   fetchMock.resetBehavior();
 };

Vulnerable code: The second parameter is the options. It accepts either a `String` of the language to translate **to** or a simple `Object` with these options:
- **`cache`**: a `Number` with the milliseconds that each translation should be cached. Leave it undefined to cache it indefinitely (until a server/browser restart).
- **`engine`**: a `String` containing the name of the engine to use for translation. Right now it defaults to `google`. Read more [in the engine section](#engines).
- **`key`**: the API Key for the engine of your choice. Read more [in the engine section](#engines).
- **`url`**: only available for those engines that you can install on your own server (like Libretranslate), allows you to specify a custom endpoint for the translations. [See this issue](https://github.com/franciscop/translate/issues/26#issuecomment-845038821) for more info.
This can be applied to any of the options enumerated above.
To pass it per-translation, you can add it to your arguments:
translate("Hello world", { to: "en", engine: "deepl", key: "YOUR-KEY-HERE" });
var iso={aar:"aa",abk:"ab",afr:"af",aka:"ak",alb:"sq",amh:"am",ara:"ar",arg:"an",arm:"hy",asm:"as",ava:"av",ave:"ae",aym:"ay",aze:"az",bak:"ba",bam:"bm",baq:"eu",bel:"be",ben:"bn",bih:"bh",bis:"bi",bos:"bs",bre:"br",bul:"bg",bur:"my",cat:"ca",cha:"ch",che:"ce",chi:"zh",chu:"cu",chv:"cv",cor:"kw",cos:"co",cre:"cr",cze:"cs",dan:"da",div:"dv",dut:"nl",dzo:"dz",eng:"en",epo:"eo",est:"et",ewe:"ee",fao:"fo",fij:"fj",fin:"fi",fre:"fr",fry:"fy",ful:"ff",geo:"ka",ger:"de",gla:"gd",gle:"ga",glg:"gl",glv:"gv",gre:"el",grn:"gn",guj:"gu",hat:"ht",hau:"ha",heb:"he",her:"hz",hin:"hi",hmo:"ho",hrv:"hr",hun:"hu",ibo:"ig",ice:"is",ido:"io",iii:"ii",iku:"iu",ile:"ie",ina:"ia",ind:"id",ipk:"ik",ita:"it",jav:"jv",jpn:"ja",kal:"kl",kan:"kn",kas:"ks",kau:"kr",kaz:"kk",khm:"km",kik:"ki",kin:"rw",kir:"ky",kom:"kv",kon:"kg",kor:"ko",kua:"kj",kur:"ku",lao:"lo",lat:"la",lav:"lv",lim:"li",lin:"ln",lit:"lt",ltz:"lb",lub:"lu",lug:"lg",mac:"mk",mah:"mh",mal:"ml",mao:"mi",mar:"mr",may:"ms",mlg:"mg",mlt:"mt",mon:"mn",nau:"na",nav:"nv",nbl:"nr",nde:"nd",ndo:"ng",nep:"ne",nno:"nn",nob:"nb",nor:"no",nya:"ny",oci:"oc",oji:"oj",ori:"or",orm:"om",oss:"os",pan:"pa",per:"fa",pli:"pi",pol:"pl",por:"pt",pus:"ps",que:"qu",roh:"rm",rum:"ro",run:"rn",rus:"ru",sag:"sg",san:"sa",sin:"si",slo:"sk",slv:"sl",sme:"se",smo:"sm",sna:"sn",snd:"sd",som:"so",sot:"st",spa:"es",srd:"sc",srp:"sr",ssw:"ss",sun:"su",swa:"sw",swe:"sv",tah:"ty",tam:"ta",tat:"tt",tel:"te",tgk:"tg",tgl:"tl",tha:"th",tib:"bo",tir:"ti",ton:"to",tsn:"tn",tso:"ts",tuk:"tk",tur:"tr",twi:"tw",uig:"ug",ukr:"uk",urd:"ur",uzb:"uz",ven:"ve",vie:"vi",vol:"vo",wel:"cy",wln:"wa",wol:"wo",xho:"xh",yid:"yi",yor:"yo",zha:"za",zul:"zu"},names={afar:"aa",abkhazian:"ab",afrikaans:"af",akan:"ak",albanian:"sq",amharic:"am",arabic:"ar",aragonese:"an",armenian:"hy",assamese:"as",avaric:"av",avestan:"ae",aymara:"ay",azerbaijani:"az",bashkir:"ba",bambara:"bm",basque:"eu",belarusian:"be",bengali:"bn","bihari languages":"bh",bislama:"bi",tibetan:"bo",bosnian:"bs",breton:"br",bulgarian:"bg",burmese:"my",catalan:"ca",valencian:"ca",czech:"cs",chamorro:"ch",chechen:"ce",chinese:"zh","church slavic":"cu","old slavonic":"cu","church slavonic":"cu","old bulgarian":"cu","old church slavonic":"cu",chuvash:"cv",cornish:"kw",corsican:"co",cree:"cr",welsh:"cy",danish:"da",german:"de",divehi:"dv",dhivehi:"dv",maldivian:"dv",dutch:"nl",flemish:"nl",dzongkha:"dz",greek:"el",english:"en",esperanto:"eo",estonian:"et",ewe:"ee",faroese:"fo",persian:"fa",fijian:"fj",finnish:"fi",french:"fr","western frisian":"fy",fulah:"ff",georgian:"ka",gaelic:"gd","scottish gaelic":"gd",irish:"ga",galician:"gl",manx:"gv",guarani:"gn",gujarati:"gu",haitian:"ht","haitian creole":"ht",hausa:"ha",hebrew:"he",herero:"hz",hindi:"hi","hiri motu":"ho",croatian:"hr",hungarian:"hu",igbo:"ig",icelandic:"is",ido:"io","sichuan yi":"ii",nuosu:"ii",inuktitut:"iu",interlingue:"ie",occidental:"ie",interlingua:"ia",indonesian:"id",inupiaq:"ik",italian:"it",javanese:"jv",japanese:"ja",kalaallisut:"kl",greenlandic:"kl",kannada:"kn",kashmiri:"ks",kanuri:"kr",kazakh:"kk","central khmer":"km",kikuyu:"ki",gikuyu:"ki",kinyarwanda:"rw",kirghiz:"ky",kyrgyz:"ky",komi:"kv",kongo:"kg",korean:"ko",kuanyama:"kj",kwanyama:"kj",kurdish:"ku",lao:"lo",latin:"la",latvian:"lv",limburgan:"li",limburger:"li",limburgish:"li",lingala:"ln",lithuanian:"lt",luxembourgish:"lb",letzeburgesch:"lb","luba-katanga":"lu",ganda:"lg",macedonian:"mk",marshallese:"mh",malayalam:"ml",maori:"mi",marathi:"mr",malay:"ms",malagasy:"mg",maltese:"mt",mongolian:"mn",nauru:"na",navajo:"nv",navaho:"nv","ndebele, south":"nr","south ndebele":"nr","ndebele, north":"nd","north ndebele":"nd",ndonga:"ng",nepali:"ne","norwegian nynorsk":"nn","nynorsk, norwegian":"nn","norwegian bokmål":"nb","bokmål, norwegian":"nb",norwegian:"no",chichewa:"ny",chewa:"ny",nyanja:"ny",occitan:"oc",ojibwa:"oj",oriya:"or",oromo:"om",ossetian:"os",ossetic:"os",panjabi:"pa",punjabi:"pa",pali:"pi",polish:"pl",portuguese:"pt",pushto:"ps",pashto:"ps",quechua:"qu",romansh:"rm",romanian:"ro",moldavian:"ro",moldovan:"ro",rundi:"rn",russian:"ru",sango:"sg",sanskrit:"sa",sinhala:"si",sinhalese:"si",slovak:"sk",slovenian:"sl","northern sami":"se",samoan:"sm",shona:"sn",sindhi:"sd",somali:"so","sotho, southern":"st",spanish:"es",castilian:"es",sardinian:"sc",serbian:"sr",swati:"ss",sundanese:"su",swahili:"sw",swedish:"sv",tahitian:"ty",tamil:"ta",tatar:"tt",telugu:"te",tajik:"tg",tagalog:"tl",thai:"th",tigrinya:"ti",tonga:"to",tswana:"tn",tsonga:"ts",turkmen:"tk",turkish:"tr",twi:"tw",uighur:"ug",uyghur:"ug",ukrainian:"uk",urdu:"ur",uzbek:"uz",venda:"ve",vietnamese:"vi","volapük":"vo",walloon:"wa",wolof:"wo",xhosa:"xh",yiddish:"yi",yoruba:"yo",zhuang:"za",chuang:"za",zulu:"zu"};const isoKeys=Object.values(iso).sort();var languages=e=>{if("string"!=typeof e)throw new Error('The "language" must be a string, received '+typeof e);if(e.length>100)throw new Error(`The "language" is too long at ${e.length} characters`);if(e=e.toLowerCase(),e=names[e]||iso[e]||e,!isoKeys.includes(e))throw new Error(`The language "${e}" is not part of the ISO 639-1`);return e};function Cache(){var e=Object.create(null);function a(a){delete e[a]}this.set=function(n,i,r){if(void 0!==r&&("number"!=typeof r||isNaN(r)||r<=0))throw new Error("Cache timeout must be a positive number");var t=e[n];t&&clearTimeout(t.timeout);var o={value:i,expire:r+Date.now()};return isNaN(o.expire)||(o.timeout=setTimeout((()=>a(n)),r)),e[n]=o,i},this.del=function(n){var i=!0,r=e[n];return r?(clearTimeout(r.timeout),!isNaN(r.expire)&&r.expire<Date.now()&&(i=!1)):i=!1,i&&a(n),i},this.clear=function(){for(var a in e)clearTimeout(e[a].timeout);e=Object.create(null)},this.get=function(a){var n=e[a];if(void 0!==n){if(isNaN(n.expire)||n.expire>=Date.now())return n.value;delete e[a]}return null}}const exp$1=new Cache;exp$1.Cache=Cache;const base="https://translate.googleapis.com/translate_a/single";var google={fetch:({key:e,from:a,to:n,text:i})=>[`${base}?client=gtx&sl=${a}&tl=${n}&dt=t&q=${encodeURI(i)}`],parse:e=>e.json().then((e=>{if(!(e=e&&e[0]&&e[0][0]&&e[0].map((e=>e[0])).join("")))throw new Error("Translation not found");return e}))},yandex={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://translate.yandex.net/api/v1.5/tr.json/translate?key=${e}&lang=${a}-${n}&text=${encodeURIComponent(i)}`,{method:"POST",body:""}],parse:e=>e.json().then((e=>{if(200!==e.code)throw new Error(e.message);return e.text[0]}))};const libreUrl="https://libretranslate.com/translate";var libre={needkey:!1,fetch:({url:e=libreUrl,key:a,from:n,to:i,text:r})=>[e,{method:"POST",body:JSON.stringify({q:r,source:n,target:i,api_key:a}),headers:{"Content-Type":"application/json"}}],parse:e=>e.json().then((e=>{if(!e)throw new Error("No response found");if(e.error)throw new Error(e.error);if(!e.translatedText)throw new Error("No response found");return e.translatedText}))},deepl={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://api${/:fx$/.test(e)?"-free":""}.deepl.com/v2/translate?auth_key=${e}&source_lang=${a}&target_lang=${n}&text=${i=encodeURIComponent(i)}`,{method:"POST",body:""}],parse:async e=>{if(!e.ok){if(403===e.status)throw new Error("Auth Error, please review the key for DeepL");throw new Error(`Error ${e.status}`)}return e.json().then((e=>e.translations[0].text))}},engines={google:google,yandex:yandex,libre:libre,deepl:deepl};const Translate=function(e={}){if(!(this instanceof Translate))return new Translate(e);const a={from:"en",to:"en",cache:void 0,languages:languages,engines:engines,engine:"google",keys:{}},n=async(e,a={})=>{"string"==typeof a&&(a={to:a}),a.text=e,a.from=languages(a.from||n.from),a.to=languages(a.to||n.to),a.cache=a.cache||n.cache,a.engines=a.engines||{},a.engine=a.engine||n.engine,a.url=a.url||n.url,a.id=a.id||`${a.url}:${a.from}:${a.to}:${a.engine}:${a.text}`,a.keys=a.keys||n.keys||{};for(let e in n.keys)a.keys[e]=a.keys[e]||n.keys[e];a.key=a.key||n.key||a.keys[a.engine];const i=a.engines[a.engine]||n.engines[a.engine],r=exp$1.get(a.id);if(r)return Promise.resolve(r);if(a.to===a.from)return Promise.resolve(a.text);if(i.needkey&&!a.key)throw new Error(`The engine "${a.engine}" needs a key, please provide it`);const t=i.fetch(a);return fetch(...t).then(i.parse).then((e=>exp$1.set(a.id,e,a.cache)))};for(let i in a)n[i]=void 0===e[i]?a[i]:e[i];return n},exp=new Translate;exp.Translate=Translate;export{exp as default};
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
    "start": "npm run watch # Start ~= Start dev",
    await translate("Is this also cached?", { to: "es", cache: 1000 });
    await translate("Is this also cached?", { to: "es" });
    const suffix = /:fx$/.test(key) ? "-free" : "";
import translate from "../../src";
    const opts = { to: "ja", engine: "google" };
    const opts = { to: "es", engine: "google" };
    const opts = { to: "pt", engine: "google" };
import translate from "../../src";
    const text = await translate("Hello world", { to: "es", engine: "libre" });
    const opts = { to: "adgdfnj", engine: "libre" };
    const opts = { to: "es", engine: "libre" };
    const opts = { to: "es", engine: "libre" };
    const opts = { to: "es", engine: "libre" };
    const opts = { to: "ja", engine: "libre" };
    const opts = { to: "es", engine: "libre" };
    const opts = { to: "ru", engine: "libre", key: "abc" };
    const opts = { to: "es", engine: "libre" };
import translate from "../../src";
    const spanish = await translate("Hello from Yandex", {
      to: "es",
      engine: "yandex",
    });
    const prom = translate("error", { to: "es", engine: "yandex" });
    const prom = translate("throw", { to: "es", engine: "yandex" });
    const opts = { to: "ja", engine: "yandex" };
    const opts = { to: "es", engine: "yandex" };
    const opts = { to: "ru", engine: "yandex", key: "abc" };
// Load a language parser to allow for more flexibility in the language choice
import languages from "./languages/index.js";


    engine: "google",
    opts.cache = opts.cache || translate.cache;
    opts.engines = opts.engines || {};
    opts.engine = opts.engine || translate.engine;
    opts.url = opts.url || translate.url;
    opts.id =
      opts.id ||
      `${opts.url}:${opts.from}:${opts.to}:${opts.engine}:${opts.text}`;
    opts.keys = opts.keys || translate.keys || {};
    const engine = opts.engines[opts.engine] || translate.engines[opts.engine];
import translate from "./";
import mock from "../test/mock";
  it("requires the key", async () => {
      translate("hello", { engine: "yandex", key: false, to: "es" })
    ).rejects.toMatchObject({
      message: 'The engine "yandex" needs a key, please provide it',
    });
  it.skip("fixed the bug #5", async () => {
    // translate.keys = { google: 'abc' };
    const options = { to: "ja", keys: { yandex: "def" }, engine: "google" };

    // This will wrongly ignore the key for "google"
    expect(await translate("Hello world", options)).toBe("こんにちは世界");
import translate from "../";
  mock(/yandex.*&lang=[a-z]*\-es/, { code: 200, text: [content] }, opts);
Fixed code: The second parameter is the options. It accepts either a `String` of the language to translate **to** or a simple `Object` with the keys `to` and `from`. However, in total there are more options, so here is a list of all of them:
- **`cache`** [instance]: a `Number` with the milliseconds that each translation should be cached. Leave it undefined to cache it indefinitely (until a server/browser restart).
- **`engine`** [instance]: a `String` containing the name of the engine to use for translation. Right now it defaults to `google`. Read more [in the engine section](#engines).
- **`key`** [instance]: the API Key for the engine of your choice. Read more [in the engine section](#engines).
- **`url`** [instance]: only available for those engines that you can install on your own server (like Libretranslate), allows you to specify a custom endpoint for the translations. [See this issue](https://github.com/franciscop/translate/issues/26#issuecomment-845038821) for more info.

> The options marked as [instance] can only be set to the root `translate.cache = 1000` or when creating a new instance `const myDeepL = translate.Translate()`

// INVALID:
const bar = await translate("Hello world", { to: "es", engine: "google" });
await translate("Hola mundo", "ja");
You can also create a new instance with different default options:

```js
const myLib = translate.Translate({ engine: 'deepl', from: 'es', ... });
await myLib("Hola mundo", "ja" );
```
You can create different instances if you want to combine different engines:
const gTranslate = translate.Translate({
  engine: "google",
  key: "YOUR-KEY-HERE",
});
const dTranslate = translate.Translate({
  engine: "deepl",
  key: "YOUR-KEY-HERE",
});
const lTranslate = translate.Translate({
  engine: "libre",
  key: "YOUR-KEY-HERE",
});

// or

const lTranslate = translate.Translate({
  engine: "libre",
  url: "...",
  key: "YOUR-KEY-HERE",
});
function Cache(){var e=Object.create(null);function a(a){delete e[a]}this.set=function(n,i,r){if(void 0!==r&&("number"!=typeof r||isNaN(r)||r<=0))throw new Error("Cache timeout must be a positive number");var t=e[n];t&&clearTimeout(t.timeout);var o={value:i,expire:r+Date.now()};return isNaN(o.expire)||(o.timeout=setTimeout((()=>a(n)),r)),e[n]=o,i},this.del=function(n){var i=!0,r=e[n];return r?(clearTimeout(r.timeout),!isNaN(r.expire)&&r.expire<Date.now()&&(i=!1)):i=!1,i&&a(n),i},this.clear=function(){for(var a in e)clearTimeout(e[a].timeout);e=Object.create(null)},this.get=function(a){var n=e[a];if(void 0!==n){if(isNaN(n.expire)||n.expire>=Date.now())return n.value;delete e[a]}return null}}const exp$1=new Cache;exp$1.Cache=Cache;const base="https://translate.googleapis.com/translate_a/single";var google={fetch:({key:e,from:a,to:n,text:i})=>[`${base}?client=gtx&sl=${a}&tl=${n}&dt=t&q=${encodeURI(i)}`],parse:e=>e.json().then((e=>{if(!(e=e&&e[0]&&e[0][0]&&e[0].map((e=>e[0])).join("")))throw new Error("Translation not found");return e}))},yandex={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://translate.yandex.net/api/v1.5/tr.json/translate?key=${e}&lang=${a}-${n}&text=${encodeURIComponent(i)}`,{method:"POST",body:""}],parse:e=>e.json().then((e=>{if(200!==e.code)throw new Error(e.message);return e.text[0]}))};const libreUrl="https://libretranslate.com/translate";var libre={needkey:!1,fetch:({url:e=libreUrl,key:a,from:n,to:i,text:r})=>[e,{method:"POST",body:JSON.stringify({q:r,source:n,target:i,api_key:a}),headers:{"Content-Type":"application/json"}}],parse:e=>e.json().then((e=>{if(!e)throw new Error("No response found");if(e.error)throw new Error(e.error);if(!e.translatedText)throw new Error("No response found");return e.translatedText}))},deepl={needkey:!0,fetch:({key:e,from:a,to:n,text:i})=>[`https://api${e.endsWith(":fx")?"-free":""}.deepl.com/v2/translate?auth_key=${e}&source_lang=${a}&target_lang=${n}&text=${i=encodeURIComponent(i)}`,{method:"POST",body:""}],parse:async e=>{if(!e.ok){if(403===e.status)throw new Error("Auth Error, please review the key for DeepL");throw new Error(`Error ${e.status}`)}return e.json().then((e=>e.translations[0].text))}},engines={google:google,yandex:yandex,libre:libre,deepl:deepl},iso={aar:"aa",abk:"ab",afr:"af",aka:"ak",alb:"sq",amh:"am",ara:"ar",arg:"an",arm:"hy",asm:"as",ava:"av",ave:"ae",aym:"ay",aze:"az",bak:"ba",bam:"bm",baq:"eu",bel:"be",ben:"bn",bih:"bh",bis:"bi",bos:"bs",bre:"br",bul:"bg",bur:"my",cat:"ca",cha:"ch",che:"ce",chi:"zh",chu:"cu",chv:"cv",cor:"kw",cos:"co",cre:"cr",cze:"cs",dan:"da",div:"dv",dut:"nl",dzo:"dz",eng:"en",epo:"eo",est:"et",ewe:"ee",fao:"fo",fij:"fj",fin:"fi",fre:"fr",fry:"fy",ful:"ff",geo:"ka",ger:"de",gla:"gd",gle:"ga",glg:"gl",glv:"gv",gre:"el",grn:"gn",guj:"gu",hat:"ht",hau:"ha",heb:"he",her:"hz",hin:"hi",hmo:"ho",hrv:"hr",hun:"hu",ibo:"ig",ice:"is",ido:"io",iii:"ii",iku:"iu",ile:"ie",ina:"ia",ind:"id",ipk:"ik",ita:"it",jav:"jv",jpn:"ja",kal:"kl",kan:"kn",kas:"ks",kau:"kr",kaz:"kk",khm:"km",kik:"ki",kin:"rw",kir:"ky",kom:"kv",kon:"kg",kor:"ko",kua:"kj",kur:"ku",lao:"lo",lat:"la",lav:"lv",lim:"li",lin:"ln",lit:"lt",ltz:"lb",lub:"lu",lug:"lg",mac:"mk",mah:"mh",mal:"ml",mao:"mi",mar:"mr",may:"ms",mlg:"mg",mlt:"mt",mon:"mn",nau:"na",nav:"nv",nbl:"nr",nde:"nd",ndo:"ng",nep:"ne",nno:"nn",nob:"nb",nor:"no",nya:"ny",oci:"oc",oji:"oj",ori:"or",orm:"om",oss:"os",pan:"pa",per:"fa",pli:"pi",pol:"pl",por:"pt",pus:"ps",que:"qu",roh:"rm",rum:"ro",run:"rn",rus:"ru",sag:"sg",san:"sa",sin:"si",slo:"sk",slv:"sl",sme:"se",smo:"sm",sna:"sn",snd:"sd",som:"so",sot:"st",spa:"es",srd:"sc",srp:"sr",ssw:"ss",sun:"su",swa:"sw",swe:"sv",tah:"ty",tam:"ta",tat:"tt",tel:"te",tgk:"tg",tgl:"tl",tha:"th",tib:"bo",tir:"ti",ton:"to",tsn:"tn",tso:"ts",tuk:"tk",tur:"tr",twi:"tw",uig:"ug",ukr:"uk",urd:"ur",uzb:"uz",ven:"ve",vie:"vi",vol:"vo",wel:"cy",wln:"wa",wol:"wo",xho:"xh",yid:"yi",yor:"yo",zha:"za",zul:"zu"},names={afar:"aa",abkhazian:"ab",afrikaans:"af",akan:"ak",albanian:"sq",amharic:"am",arabic:"ar",aragonese:"an",armenian:"hy",assamese:"as",avaric:"av",avestan:"ae",aymara:"ay",azerbaijani:"az",bashkir:"ba",bambara:"bm",basque:"eu",belarusian:"be",bengali:"bn","bihari languages":"bh",bislama:"bi",tibetan:"bo",bosnian:"bs",breton:"br",bulgarian:"bg",burmese:"my",catalan:"ca",valencian:"ca",czech:"cs",chamorro:"ch",chechen:"ce",chinese:"zh","church slavic":"cu","old slavonic":"cu","church slavonic":"cu","old bulgarian":"cu","old church slavonic":"cu",chuvash:"cv",cornish:"kw",corsican:"co",cree:"cr",welsh:"cy",danish:"da",german:"de",divehi:"dv",dhivehi:"dv",maldivian:"dv",dutch:"nl",flemish:"nl",dzongkha:"dz",greek:"el",english:"en",esperanto:"eo",estonian:"et",ewe:"ee",faroese:"fo",persian:"fa",fijian:"fj",finnish:"fi",french:"fr","western frisian":"fy",fulah:"ff",georgian:"ka",gaelic:"gd","scottish gaelic":"gd",irish:"ga",galician:"gl",manx:"gv",guarani:"gn",gujarati:"gu",haitian:"ht","haitian creole":"ht",hausa:"ha",hebrew:"he",herero:"hz",hindi:"hi","hiri motu":"ho",croatian:"hr",hungarian:"hu",igbo:"ig",icelandic:"is",ido:"io","sichuan yi":"ii",nuosu:"ii",inuktitut:"iu",interlingue:"ie",occidental:"ie",interlingua:"ia",indonesian:"id",inupiaq:"ik",italian:"it",javanese:"jv",japanese:"ja",kalaallisut:"kl",greenlandic:"kl",kannada:"kn",kashmiri:"ks",kanuri:"kr",kazakh:"kk","central khmer":"km",kikuyu:"ki",gikuyu:"ki",kinyarwanda:"rw",kirghiz:"ky",kyrgyz:"ky",komi:"kv",kongo:"kg",korean:"ko",kuanyama:"kj",kwanyama:"kj",kurdish:"ku",lao:"lo",latin:"la",latvian:"lv",limburgan:"li",limburger:"li",limburgish:"li",lingala:"ln",lithuanian:"lt",luxembourgish:"lb",letzeburgesch:"lb","luba-katanga":"lu",ganda:"lg",macedonian:"mk",marshallese:"mh",malayalam:"ml",maori:"mi",marathi:"mr",malay:"ms",malagasy:"mg",maltese:"mt",mongolian:"mn",nauru:"na",navajo:"nv",navaho:"nv","ndebele, south":"nr","south ndebele":"nr","ndebele, north":"nd","north ndebele":"nd",ndonga:"ng",nepali:"ne","norwegian nynorsk":"nn","nynorsk, norwegian":"nn","norwegian bokmål":"nb","bokmål, norwegian":"nb",norwegian:"no",chichewa:"ny",chewa:"ny",nyanja:"ny",occitan:"oc",ojibwa:"oj",oriya:"or",oromo:"om",ossetian:"os",ossetic:"os",panjabi:"pa",punjabi:"pa",pali:"pi",polish:"pl",portuguese:"pt",pushto:"ps",pashto:"ps",quechua:"qu",romansh:"rm",romanian:"ro",moldavian:"ro",moldovan:"ro",rundi:"rn",russian:"ru",sango:"sg",sanskrit:"sa",sinhala:"si",sinhalese:"si",slovak:"sk",slovenian:"sl","northern sami":"se",samoan:"sm",shona:"sn",sindhi:"sd",somali:"so","sotho, southern":"st",spanish:"es",castilian:"es",sardinian:"sc",serbian:"sr",swati:"ss",sundanese:"su",swahili:"sw",swedish:"sv",tahitian:"ty",tamil:"ta",tatar:"tt",telugu:"te",tajik:"tg",tagalog:"tl",thai:"th",tigrinya:"ti",tonga:"to",tswana:"tn",tsonga:"ts",turkmen:"tk",turkish:"tr",twi:"tw",uighur:"ug",uyghur:"ug",ukrainian:"uk",urdu:"ur",uzbek:"uz",venda:"ve",vietnamese:"vi","volapük":"vo",walloon:"wa",wolof:"wo",xhosa:"xh",yiddish:"yi",yoruba:"yo",zhuang:"za",chuang:"za",zulu:"zu"};const isoKeys=Object.values(iso).sort();var languages=e=>{if("string"!=typeof e)throw new Error('The "language" must be a string, received '+typeof e);if(e.length>100)throw new Error(`The "language" is too long at ${e.length} characters`);if(e=e.toLowerCase(),e=names[e]||iso[e]||e,!isoKeys.includes(e))throw new Error(`The language "${e}" is not part of the ISO 639-1`);return e};const Translate=function(e={}){if(!(this instanceof Translate))return new Translate(e);const a={from:"en",to:"en",cache:void 0,engine:"google",key:void 0,url:void 0,languages:languages,engines:engines,keys:{}},n=async(e,a={})=>{"string"==typeof a&&(a={to:a});const i=Object.keys(a).find((e=>"from"!==e&&"to"!==e));if(i)throw new Error(`Invalid option with the name '${i}'`);a.text=e,a.from=languages(a.from||n.from),a.to=languages(a.to||n.to),a.cache=n.cache,a.engine=n.engine,a.url=n.url,a.id=`${a.url}:${a.from}:${a.to}:${a.engine}:${a.text}`,a.keys=n.keys||{};for(let e in n.keys)a.keys[e]=a.keys[e]||n.keys[e];a.key=a.key||n.key||a.keys[a.engine];const r=n.engines[a.engine],t=exp$1.get(a.id);if(t)return Promise.resolve(t);if(a.to===a.from)return Promise.resolve(a.text);if(r.needkey&&!a.key)throw new Error(`The engine "${a.engine}" needs a key, please provide it`);const o=r.fetch(a);return fetch(...o).then(r.parse).then((e=>exp$1.set(a.id,e,a.cache)))};for(let i in a)n[i]=void 0===e[i]?a[i]:e[i];return n},exp=new Translate;exp.Translate=Translate;export{exp as default};
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
    "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
    const t = translate.Translate({ cache: 1000 });
    await t("Is this also cached?", { to: "es" });
    await t("Is this also cached?", { to: "es" });
    const suffix = key.endsWith(":fx") ? "-free" : "";

import t from "../../src";
const translate = t.Translate({ engine: "google" });

    const opts = { to: "ja" };
    const opts = { to: "es" };
    const opts = { to: "pt" };

import t from "../../src";
const translate = t.Translate({ engine: "libre" });
    const text = await translate("Hello world", { to: "es" });
    const opts = { to: "adgdfnj" };
    const opts = { to: "es" };
    const opts = { to: "es" };
    const opts = { to: "es" };
    const opts = { to: "ja" };
    const opts = { to: "es" };
    const opts = { to: "ru", key: "abc" };
    const opts = { to: "es" };

import t from "../../src";
const translate = t.Translate({ engine: "yandex" });
    const spanish = await translate("Hello from Yandex", { to: "es" });
    const prom = translate("error", { to: "es" });
    const prom = translate("throw", { to: "es" });
    const opts = { to: "ja" };
    const opts = { to: "es" };
    const opts = { to: "ru", key: "abc" };
// Load a language parser to allow for more flexibility in the language choice
import languages from "./languages/index.js";
    engine: "google",
    key: undefined,
    url: undefined,
    const invalidKey = Object.keys(opts).find(
      (k) => k !== "from" && k !== "to"
    );
    if (invalidKey) {
      throw new Error(`Invalid option with the name '${invalidKey}'`);
    }
    opts.cache = translate.cache;
    opts.engine = translate.engine;
    opts.url = translate.url;
    opts.id = `${opts.url}:${opts.from}:${opts.to}:${opts.engine}:${opts.text}`;
    opts.keys = translate.keys || {};
    const engine = translate.engines[opts.engine];

import mock from "../test/mock";
import translate from "./";

  it("does not accept other keys", async () => {
      translate("hello", { to: "es", blabla: "blu" })
    ).rejects.toThrow("Invalid option with the name 'blabla'");
  });

  it("requires the key", async () => {
    const yandex = translate.Translate({ engine: "yandex", key: false });
    await expect(() => yandex("hello", "es")).rejects.toThrow(
      'The engine "yandex" needs a key, please provide it'
    );
  it("cannot poison the cache", async () => {
    const options = {
      id: "undefined:en:nl:google:I love you",
      from: "en",
      to: "ja",
    };
    await expect(() => translate("hello", options)).rejects.toThrow(
      "Invalid option with the name 'id'"
    );
import translate from "..";
  mock(/yandex.*&lang=[a-z]*-es/, { code: 200, text: [content] }, opts);