Skip to content

Commit

Permalink
Made the usage options much simpler so that there's no chance of writ…
Browse files Browse the repository at this point in the history
…ing runtime vulnerabilities
  • Loading branch information
franciscop committed Mar 21, 2024
1 parent e6b831c commit 7a2bf8b
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 67 deletions.
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ console.log(text);

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:

Expand All @@ -78,6 +80,9 @@ 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.
Expand All @@ -89,9 +94,15 @@ You can change the default options for anything by calling the root library and
```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

Expand Down Expand Up @@ -126,17 +137,36 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion index.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"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",
Expand Down
5 changes: 3 additions & 2 deletions src/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ describe("cache", () => {
});

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);
Expand Down
2 changes: 1 addition & 1 deletion src/engines/deepl.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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}`,
Expand Down
11 changes: 7 additions & 4 deletions src/engines/google.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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());

Expand Down Expand Up @@ -39,17 +42,17 @@ describe("google full requests", () => {
});

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
Expand Down
22 changes: 12 additions & 10 deletions src/engines/libre.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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", () => {
Expand All @@ -10,27 +12,27 @@ describe("Libre mocked requests", () => {

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",
});
Expand All @@ -40,7 +42,7 @@ describe("Libre mocked requests", () => {
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",
});
Expand All @@ -59,23 +61,23 @@ describe("libre full requests", () => {
}

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");
Expand Down
19 changes: 9 additions & 10 deletions src/engines/yandex.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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", () => {
Expand All @@ -18,20 +20,17 @@ describe("yandex mocked requests", () => {

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");
});
});
Expand All @@ -46,17 +45,17 @@ describe("yandex full requests", () => {
}

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();
});
});
31 changes: 17 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// 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 = {}) {
Expand All @@ -19,35 +17,40 @@ const Translate = function (options = {}) {
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);
Expand Down
Loading

0 comments on commit 7a2bf8b

Please sign in to comment.