mirror of
https://github.com/thedaviddelta/lingva-scraper.git
synced 2025-10-05 15:52:40 +02:00
General refactor and simple translation implementation
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"user-agents": "^1.0.1033"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
19
src/audio.ts
19
src/audio.ts
@@ -1,13 +1,28 @@
|
||||
import { mapGoogleCode, LangCode } from "./utils/language";
|
||||
import request, { Endpoint } from "./utils/request";
|
||||
|
||||
export const getAudio = (lang: LangCode<"target">, text: string) => {
|
||||
/**
|
||||
* Retrieves an audio buffer of the given text in the given language
|
||||
* @param lang - The code of the language to be used as the TTS voice
|
||||
* @param text - The text to be converted into speech
|
||||
* @param [isSlow=false] - Whether the audio should be slowed down or not
|
||||
* @returns An array of numbers that represents a {@link Uint8Array} of the audio buffer
|
||||
*/
|
||||
export const getAudio = async (
|
||||
lang: LangCode<"target">,
|
||||
text: string,
|
||||
isSlow: boolean = false
|
||||
): Promise<number[] | null> => {
|
||||
const parsedLang = mapGoogleCode(lang);
|
||||
|
||||
const lastSpace = text.lastIndexOf(" ", 200);
|
||||
const slicedText = text.slice(0, text.length > 200 && lastSpace !== -1 ? lastSpace : 200);
|
||||
const encodedText = encodeURIComponent(slicedText);
|
||||
|
||||
const textLength = slicedText.length;
|
||||
const speed = isSlow ? 0.1 : 1;
|
||||
|
||||
return request(Endpoint.AUDIO)
|
||||
.with({ lang: parsedLang, text: slicedText })
|
||||
.with({ lang: parsedLang, text: encodedText, textLength, speed })
|
||||
.doing(({ data }) => data ? Array.from(new Uint8Array(data)) : null);
|
||||
};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export * from "./info";
|
||||
export * from "./simple";
|
||||
export * from "./audio";
|
||||
export * from "./translation";
|
||||
export * from "./utils/language";
|
||||
export * from "./utils/interfaces";
|
||||
|
51
src/info.ts
Normal file
51
src/info.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { mapGoogleCode, LangCode } from "./utils/language";
|
||||
import request, { Endpoint } from "./utils/request";
|
||||
import * as parse from "./utils/parse";
|
||||
import { Boilerplate, Data } from "./utils/types";
|
||||
import { TranslationInfo } from "./utils/interfaces";
|
||||
|
||||
/**
|
||||
* Retrieves the full translation information, including the translated text and, optionally, other relevant data
|
||||
* @param source - The code of the language to translate from
|
||||
* @param target - The code of the language to translate to
|
||||
* @param query - The text to be translated
|
||||
* @returns An element with all the information, as defined in {@link TranslationInfo}
|
||||
*/
|
||||
export const getTranslationInfo = async (
|
||||
source: LangCode<"source">,
|
||||
target: LangCode<"target">,
|
||||
query: string
|
||||
): Promise<TranslationInfo | null> => {
|
||||
const parsedSource = mapGoogleCode(source);
|
||||
const parsedTarget = mapGoogleCode(target);
|
||||
|
||||
const reqData = JSON.stringify([[query, parsedSource, parsedTarget, true], [null]]);
|
||||
const reqBoilerplate = JSON.stringify([[["MkEWBc", reqData, null, "generic"]]]);
|
||||
const body = "f.req=" + encodeURIComponent(reqBoilerplate);
|
||||
|
||||
return request(Endpoint.INFO)
|
||||
.with({ body })
|
||||
.doing(({ data }) => {
|
||||
const resBoilerplate: Boilerplate = JSON.parse(data?.split("\n")?.[3]);
|
||||
const resData: Data = JSON.parse(resBoilerplate?.[0]?.[2]);
|
||||
if (!resData)
|
||||
return;
|
||||
|
||||
const translation = parse.translation(resData);
|
||||
if (!translation)
|
||||
return;
|
||||
|
||||
return parse.undefinedFields({
|
||||
translation,
|
||||
detectedSource: parse.detected(resData),
|
||||
pronunciation: {
|
||||
query: parse.pronunciation.query(resData),
|
||||
translation: parse.pronunciation.translation(resData)
|
||||
},
|
||||
definitions: parse.list.definitions(resData),
|
||||
examples: parse.list.examples(resData),
|
||||
similar: parse.list.similar(resData),
|
||||
extraTranslations: parse.list.translations(resData),
|
||||
});
|
||||
});
|
||||
};
|
36
src/simple.ts
Normal file
36
src/simple.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import cheerio from "cheerio";
|
||||
import { mapGoogleCode, LangCode } from "./utils/language";
|
||||
import request, { Endpoint } from "./utils/request";
|
||||
|
||||
/**
|
||||
* Retrieves the translation text using a legacy implementation
|
||||
* @param source - The code of the language to translate from
|
||||
* @param target - The code of the language to translate to
|
||||
* @param query - The text to be translated
|
||||
* @returns A single {@link string} with the translated text
|
||||
*/
|
||||
export const getSimpleTranslation = async (
|
||||
source: LangCode<"source">,
|
||||
target: LangCode<"target">,
|
||||
query: string
|
||||
): Promise<string | null> => {
|
||||
const parsedSource = mapGoogleCode(source);
|
||||
const parsedTarget = mapGoogleCode(target);
|
||||
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
|
||||
if (encodedQuery.length > 7500)
|
||||
return null;
|
||||
|
||||
return request(Endpoint.SIMPLE)
|
||||
.with({ source: parsedSource, target: parsedTarget, query: encodedQuery })
|
||||
.doing(({ data }) => {
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
const translation = cheerio.load(data)(".result-container").text()?.trim();
|
||||
return translation && !translation.includes("#af-error-page")
|
||||
? translation
|
||||
: null;
|
||||
});
|
||||
};
|
@@ -1,44 +0,0 @@
|
||||
import { mapGoogleCode, LangCode } from "./utils/language";
|
||||
import request, { Endpoint } from "./utils/request";
|
||||
import * as parse from "./utils/parse";
|
||||
import { Boilerplate, Info } from "./utils/types";
|
||||
import { Translation } from "./utils/interfaces";
|
||||
|
||||
export const getTranslation = (
|
||||
source: LangCode<"source">,
|
||||
target: LangCode<"target">,
|
||||
query: string
|
||||
): Promise<Translation | null> => {
|
||||
const parsedSource = mapGoogleCode(source);
|
||||
const parsedTarget = mapGoogleCode(target);
|
||||
|
||||
const reqInfo = JSON.stringify([[query, parsedSource, parsedTarget, true], [null]]);
|
||||
const reqBoilerplate = JSON.stringify([[["MkEWBc", reqInfo, null, "generic"]]]);
|
||||
const body = "f.req=" + encodeURIComponent(reqBoilerplate);
|
||||
|
||||
return request(Endpoint.TRANSLATION)
|
||||
.with({ body })
|
||||
.doing(({ data }) => {
|
||||
const boilerplate: Boilerplate = JSON.parse(data?.split("\n")?.[3]);
|
||||
const info: Info = JSON.parse(boilerplate?.[0]?.[2]);
|
||||
if (!info)
|
||||
return;
|
||||
|
||||
const translation = parse.translation(info);
|
||||
if (!translation)
|
||||
return;
|
||||
|
||||
return parse.undefinedFields({
|
||||
translation,
|
||||
detectedSource: parse.detected(info),
|
||||
pronunciation: {
|
||||
query: parse.pronunciation.query(info),
|
||||
translation: parse.pronunciation.translation(info)
|
||||
},
|
||||
definitions: parse.list.definitions(info),
|
||||
examples: parse.list.examples(info),
|
||||
similar: parse.list.similar(info),
|
||||
extraTranslations: parse.list.translations(info),
|
||||
});
|
||||
});
|
||||
};
|
@@ -20,13 +20,13 @@ interface ExtraTranslationsGroup {
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Translation {
|
||||
export interface TranslationInfo {
|
||||
translation: string,
|
||||
detectedSource?: LangCode<"source">,
|
||||
pronunciation: {
|
||||
query?: string,
|
||||
translation?: string
|
||||
}
|
||||
},
|
||||
definitions: DefinitionsGroup[],
|
||||
examples: string[],
|
||||
similar: string[],
|
||||
|
@@ -19,6 +19,12 @@ export type LangCodeGoogle<T extends LangCode | LangType = LangCode> =
|
||||
|
||||
const isKeyOf = <T extends object>(obj: T) => (key: keyof any): key is keyof T => key in obj;
|
||||
|
||||
/**
|
||||
* Changes invalid languages for a certain language type with the proper replacement
|
||||
* @param langType - The type of language to check on
|
||||
* @param langCode - A *Lingva* language code
|
||||
* @returns A proper *Lingva* code for that language type
|
||||
*/
|
||||
export const replaceExceptedCode = <T extends LangType>(langType: T, langCode: LangCode) => {
|
||||
const langExceptions = exceptions[langType];
|
||||
const finalCode = isKeyOf(langExceptions)(langCode)
|
||||
@@ -27,6 +33,11 @@ export const replaceExceptedCode = <T extends LangType>(langType: T, langCode: L
|
||||
return finalCode as LangCode<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a *Lingva* language code to a *Google* one
|
||||
* @param langCode - A *Lingva* language code
|
||||
* @returns The proper *Google* code for that language
|
||||
*/
|
||||
export const mapGoogleCode = <T extends LangCode>(langCode: T) => {
|
||||
const reqMappings = mappings["request"];
|
||||
const finalCode = isKeyOf(reqMappings)(langCode)
|
||||
@@ -35,6 +46,11 @@ export const mapGoogleCode = <T extends LangCode>(langCode: T) => {
|
||||
return finalCode as LangCodeGoogle<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a *Google* language code to a *Lingva* one
|
||||
* @param langCode - A *Google* language code
|
||||
* @returns The proper *Lingva* code for that language
|
||||
*/
|
||||
export const mapLingvaCode = <T extends LangType>(langCode: LangCodeGoogle<T>) => {
|
||||
const resMappings = mappings["response"];
|
||||
const finalCode = isKeyOf(resMappings)(langCode)
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import { Info } from "./types";
|
||||
import { Data } from "./types";
|
||||
import { mapLingvaCode } from "./language";
|
||||
import { Translation } from "./interfaces";
|
||||
import { TranslationInfo } from "./interfaces";
|
||||
|
||||
export const detected = ([source, target, detected, extra]: Info): Translation["detectedSource"] => {
|
||||
export const detected = ([source, target, detected, extra]: Data): TranslationInfo["detectedSource"] => {
|
||||
const code = detected ?? source?.[2] ?? target?.[3] ?? extra?.[8] ?? extra?.[5]?.[0]?.[0]?.[3];
|
||||
return code ? mapLingvaCode<"source">(code) : undefined;
|
||||
};
|
||||
|
||||
export const translation = ([, target]: Info): Translation["translation"] | undefined => (
|
||||
export const translation = ([, target]: Data): TranslationInfo["translation"] | undefined => (
|
||||
target?.[0]?.[0]?.[5]?.[0]?.[0] ?? target?.[0]?.[0]?.[5]?.[0]?.[4]?.[0]?.[0]
|
||||
);
|
||||
|
||||
export const pronunciation = {
|
||||
query: ([source]: Info): Translation["pronunciation"]["query"] => (
|
||||
query: ([source]: Data): TranslationInfo["pronunciation"]["query"] => (
|
||||
source?.[0] ?? undefined
|
||||
),
|
||||
translation: ([, target]: Info): Translation["pronunciation"]["translation"] => (
|
||||
translation: ([, target]: Data): TranslationInfo["pronunciation"]["translation"] => (
|
||||
target?.[0]?.[0]?.[1] ?? undefined
|
||||
)
|
||||
};
|
||||
|
||||
export const list = {
|
||||
definitions: ({ 3: extra }: Info): Translation["definitions"] => (
|
||||
definitions: ({ 3: extra }: Data): TranslationInfo["definitions"] => (
|
||||
extra?.[1]?.[0]?.map(([type, defList]) => ({
|
||||
type,
|
||||
list: defList?.map(({ 0: definition, 1: example, 4: fieldWrapper, 5: synList }) => ({
|
||||
@@ -34,13 +34,13 @@ export const list = {
|
||||
})) ?? []
|
||||
})) ?? []
|
||||
),
|
||||
examples: ({ 3: extra }: Info): Translation["examples"] => (
|
||||
examples: ({ 3: extra }: Data): TranslationInfo["examples"] => (
|
||||
extra?.[2]?.[0]?.map(([, item]) => item) ?? []
|
||||
),
|
||||
similar: ({ 3: extra }: Info): Translation["similar"] => (
|
||||
similar: ({ 3: extra }: Data): TranslationInfo["similar"] => (
|
||||
extra?.[3]?.[0] ?? []
|
||||
),
|
||||
translations: ({ 3: extra }: Info): Translation["extraTranslations"] => (
|
||||
translations: ({ 3: extra }: Data): TranslationInfo["extraTranslations"] => (
|
||||
extra?.[5]?.[0]?.map(([type, transList]) => ({
|
||||
type,
|
||||
list: transList?.map(([word, article, meanings, frequency]) => ({
|
||||
|
@@ -3,19 +3,27 @@ import UserAgent from "user-agents";
|
||||
import { LangCodeGoogle } from "./language";
|
||||
|
||||
export const Endpoint = {
|
||||
TRANSLATION: "translation",
|
||||
INFO: "info",
|
||||
SIMPLE: "simple",
|
||||
AUDIO: "audio"
|
||||
} as const;
|
||||
|
||||
type EndpointType = typeof Endpoint[keyof typeof Endpoint];
|
||||
|
||||
type Params = {
|
||||
translation: {
|
||||
body: string,
|
||||
[Endpoint.INFO]: {
|
||||
body: string
|
||||
},
|
||||
audio: {
|
||||
[Endpoint.SIMPLE]: {
|
||||
source: LangCodeGoogle<"source">,
|
||||
target: LangCodeGoogle<"target">,
|
||||
query: string
|
||||
},
|
||||
[Endpoint.AUDIO]: {
|
||||
lang: LangCodeGoogle<"target">,
|
||||
text: string
|
||||
text: string,
|
||||
textLength: number,
|
||||
speed: number
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,9 +34,7 @@ const request = <T extends EndpointType>(
|
||||
with: (
|
||||
params: Params[T]
|
||||
) => {
|
||||
const promise = endpoint === "translation"
|
||||
? fetchTranslation(params as Params["translation"])
|
||||
: fetchAudio(params as Params["audio"]);
|
||||
const promise = retrieve(endpoint, params);
|
||||
return {
|
||||
promise,
|
||||
doing: <V>(
|
||||
@@ -50,29 +56,47 @@ const isEmpty = (item: any) => (
|
||||
!item || (typeof item === "object" && "length" in item && item.length <= 0)
|
||||
);
|
||||
|
||||
const fetchTranslation = ({ body }: Params["translation"]) => (
|
||||
axios.post(
|
||||
"https://translate.google.com/_/TranslateWebserverUi/data/batchexecute?rpcids=MkEWBc&rt=c",
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": new UserAgent().toString(),
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
const retrieve = <T extends EndpointType>(endpoint: T, params: Params[T]) => {
|
||||
if (endpoint === Endpoint.INFO) {
|
||||
const { body } = params as Params[typeof Endpoint.INFO];
|
||||
return axios.post(
|
||||
"https://translate.google.com/_/TranslateWebserverUi/data/batchexecute?rpcids=MkEWBc&rt=c",
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": new UserAgent().toString(),
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const fetchAudio = ({ lang, text }: Params["audio"]) => (
|
||||
axios.get(
|
||||
`https://translate.google.com/translate_tts?tl=${lang}&q=${encodeURIComponent(text)}&textlen=${text.length}&client=tw-ob`,
|
||||
{
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
"User-Agent": new UserAgent().toString()
|
||||
if (endpoint === Endpoint.SIMPLE) {
|
||||
const { source, target, query } = params as Params[typeof Endpoint.SIMPLE];
|
||||
return axios.get(
|
||||
`https://translate.google.com/m?sl=${source}&tl=${target}&q=${query}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": new UserAgent().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (endpoint === Endpoint.AUDIO) {
|
||||
const { lang, text, textLength, speed } = params as Params[typeof Endpoint.AUDIO];
|
||||
return axios.get(
|
||||
`https://translate.google.com/translate_tts?tl=${lang}&q=${text}&textlen=${textLength}&speed=${speed}&client=tw-ob`,
|
||||
{
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
"User-Agent": new UserAgent().toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("Invalid endpoint");
|
||||
};
|
||||
|
||||
export default request;
|
||||
|
@@ -4,7 +4,7 @@ export type Boilerplate = [
|
||||
[
|
||||
"wrb.fr",
|
||||
"MkEWBc",
|
||||
string, // info
|
||||
string, // data
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -22,14 +22,14 @@ export type Boilerplate = [
|
||||
]
|
||||
];
|
||||
|
||||
export type Info = [
|
||||
InfoSource,
|
||||
InfoTarget,
|
||||
export type Data = [
|
||||
DataSource,
|
||||
DataTarget,
|
||||
LangCodeGoogle<"source">, // source/detected
|
||||
InfoExtra | undefined
|
||||
DataExtra | undefined
|
||||
];
|
||||
|
||||
type InfoSource = [
|
||||
type DataSource = [
|
||||
string | null, // pronunciation
|
||||
[
|
||||
null,
|
||||
@@ -67,7 +67,7 @@ type InfoSource = [
|
||||
]
|
||||
];
|
||||
|
||||
type InfoTarget = [ // target
|
||||
type DataTarget = [ // target
|
||||
[
|
||||
[
|
||||
null,
|
||||
@@ -100,7 +100,7 @@ type InfoTarget = [ // target
|
||||
]
|
||||
];
|
||||
|
||||
type InfoExtra = [
|
||||
type DataExtra = [
|
||||
string, // query
|
||||
[ // definitions
|
||||
[
|
||||
|
@@ -1,23 +1,26 @@
|
||||
import { getAudio, LangCode } from "../src";
|
||||
// @ts-ignore
|
||||
import { expectFromEntries, expectFromWrong, sampleText } from "./testUtils";
|
||||
|
||||
const entries: [LangCode<"target">, string][] = [
|
||||
type Entry = [LangCode<"target">, string];
|
||||
|
||||
const entries: Entry[] = [
|
||||
["es", "hola"],
|
||||
["ca", "gerd"],
|
||||
["en", "impression"],
|
||||
["zh", "早安"],
|
||||
["zh_HANT", "早安"]
|
||||
["zh_HANT", "早安"],
|
||||
["ca", sampleText.large],
|
||||
["ca", sampleText.huge]
|
||||
];
|
||||
|
||||
it("returns audio buffer correctly", async () => (
|
||||
Promise.all(entries.map((entry) => getAudio(...entry)))
|
||||
.then(results => results.forEach(audio => {
|
||||
expect(audio).not.toBeNull();
|
||||
audio?.forEach(int => expect(int).toEqual(expect.any(Number)));
|
||||
}))
|
||||
it("returns audio buffer correctly", () => (
|
||||
expectFromEntries(entries, getAudio, audio => {
|
||||
expect(audio).not.toBeNull();
|
||||
audio?.forEach(int => expect(int).toEqual(expect.any(Number)));
|
||||
})
|
||||
));
|
||||
|
||||
it("returns null on wrong params", async () => (
|
||||
// @ts-ignore
|
||||
getAudio("", "")
|
||||
.then(audio => expect(audio).toBeNull())
|
||||
it("returns null on wrong params", () => (
|
||||
expectFromWrong(getAudio)
|
||||
));
|
||||
|
30
tests/info.test.ts
Normal file
30
tests/info.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getTranslationInfo, LangCode } from "../src";
|
||||
// @ts-ignore
|
||||
import { expectFromEntries, expectFromWrong, expectTruthyValues, sampleText } from "./testUtils";
|
||||
|
||||
type Entry = [LangCode<"source">, LangCode<"target">, string];
|
||||
|
||||
const entries: Entry[] = [
|
||||
["es", "en", "hola"],
|
||||
["es", "en", "convención"],
|
||||
["auto", "en", "prueba"],
|
||||
["auto", "es", "win"],
|
||||
["zh", "en", "早安"],
|
||||
["ca", "es", sampleText.large]
|
||||
];
|
||||
|
||||
it("returns translation info correctly", () => (
|
||||
expectFromEntries(entries, getTranslationInfo, info => {
|
||||
expect(info).not.toBeNull();
|
||||
info && expectTruthyValues(info);
|
||||
})
|
||||
));
|
||||
|
||||
it("returns null on wrong params", () => (
|
||||
expectFromWrong(getTranslationInfo)
|
||||
));
|
||||
|
||||
it("returns null on huge text", () => (
|
||||
getTranslationInfo("ca", "es", sampleText.huge)
|
||||
.then(info => expect(info).toBeNull())
|
||||
));
|
29
tests/simple.test.ts
Normal file
29
tests/simple.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getSimpleTranslation, LangCode } from "../src";
|
||||
// @ts-ignore
|
||||
import { expectFromEntries, expectFromWrong, sampleText } from "./testUtils";
|
||||
|
||||
type Entry = [LangCode<"source">, LangCode<"target">, string];
|
||||
|
||||
const entries: Entry[] = [
|
||||
["es", "en", "hola"],
|
||||
["es", "en", "convención"],
|
||||
["auto", "en", "prueba"],
|
||||
["auto", "es", "win"],
|
||||
["zh", "en", "早安"],
|
||||
["ca", "es", sampleText.large]
|
||||
];
|
||||
|
||||
it("returns translated text correctly", () => (
|
||||
expectFromEntries(entries, getSimpleTranslation, trans => {
|
||||
expect(trans).not.toBeNull();
|
||||
})
|
||||
));
|
||||
|
||||
it("returns null on wrong params", () => (
|
||||
expectFromWrong(getSimpleTranslation)
|
||||
));
|
||||
|
||||
it("returns null on huge text", () => (
|
||||
getSimpleTranslation("ca", "es", sampleText.huge)
|
||||
.then(trans => expect(trans).toBeNull())
|
||||
));
|
32
tests/testUtils.ts
Normal file
32
tests/testUtils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { EOL } from "node:os";
|
||||
import tirant from "./tirant.json";
|
||||
|
||||
export const expectFromEntries = <E extends any[], R>(
|
||||
entries: E[],
|
||||
runCb: (...params: E) => Promise<R>,
|
||||
expectCb: (result: R) => any
|
||||
): Promise<void> => (
|
||||
Promise.all(entries.map(entry => runCb(...entry)))
|
||||
.then(results => results.forEach(expectCb))
|
||||
);
|
||||
|
||||
export const expectFromWrong = async <E extends any[]>(
|
||||
callback: (...params: E) => Promise<any>
|
||||
): Promise<void> => (
|
||||
callback(...Array(10).fill("") as E)
|
||||
.then(result => expect(result).toBeNull())
|
||||
);
|
||||
|
||||
export const expectTruthyValues = (obj: object): void => {
|
||||
const list = Array.isArray(obj) ? obj : Object.values(obj);
|
||||
list.forEach(value =>
|
||||
typeof value === "object"
|
||||
? expectTruthyValues(value)
|
||||
: expect(value).toBeTruthy()
|
||||
);
|
||||
};
|
||||
|
||||
export const sampleText = {
|
||||
large: tirant.prologue,
|
||||
huge: tirant.chapters.join(EOL)
|
||||
};
|
10
tests/tirant.json
Normal file
10
tests/tirant.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
||||
import { getTranslation, LangCode } from "../src";
|
||||
|
||||
const entries: [LangCode<"source">, LangCode<"target">, string][] = [
|
||||
["es", "en", "hola"],
|
||||
["es", "en", "convención"],
|
||||
["auto", "en", "prueba"],
|
||||
["auto", "es", "win"],
|
||||
["zh", "en", "早安"]
|
||||
];
|
||||
|
||||
export const expectNoUndefined = (obj: object): void => {
|
||||
const list = Array.isArray(obj) ? obj : Object.values(obj);
|
||||
list.forEach(value =>
|
||||
typeof value === "object"
|
||||
? expectNoUndefined(value)
|
||||
: expect(value).toBeTruthy()
|
||||
);
|
||||
};
|
||||
|
||||
it("returns translation info correctly", async () => (
|
||||
Promise.all(entries.map((entry) => getTranslation(...entry)))
|
||||
.then(results => results.forEach(trans => {
|
||||
expect(trans).not.toBeNull();
|
||||
trans && expectNoUndefined(trans);
|
||||
}))
|
||||
));
|
||||
|
||||
it("returns null on wrong params", async () => (
|
||||
// @ts-ignore
|
||||
getTranslation("", "", "")
|
||||
.then(trans => expect(trans).toBeNull())
|
||||
));
|
119
yarn.lock
119
yarn.lock
@@ -774,6 +774,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -861,6 +866,32 @@ char-regex@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
||||
|
||||
cheerio-select@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
|
||||
integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-select "^5.1.0"
|
||||
css-what "^6.1.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
|
||||
cheerio@^1.0.0-rc.11:
|
||||
version "1.0.0-rc.11"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.11.tgz#1be84be1a126958366bcc57a11648cd9b30a60c2"
|
||||
integrity sha512-bQwNaDIBKID5ts/DsdhxrjqFXYfLw4ste+wMKqWA8DyKcS4qwsPP4Bk8ZNaTJjvpiX/qW3BT4sU7d6Bh5i+dag==
|
||||
dependencies:
|
||||
cheerio-select "^2.1.0"
|
||||
dom-serializer "^2.0.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
htmlparser2 "^8.0.1"
|
||||
parse5 "^7.0.0"
|
||||
parse5-htmlparser2-tree-adapter "^7.0.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
ci-info@^3.2.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.1.tgz#58331f6f472a25fe3a50a351ae3052936c2c7f32"
|
||||
@@ -942,6 +973,22 @@ cross-spawn@^7.0.3:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-what@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
@@ -989,6 +1036,36 @@ docopt@~0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz#b28e9e2220da5ec49f7ea5bb24a47787405eeb11"
|
||||
integrity sha512-NqTbaYeE4gA/wU1hdKFdU+AFahpDOpgGLzHP42k6H6DKExJd0A55KEVWYhL9FEmHmgeLvEU2vuKXDuU+4yToOw==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
|
||||
integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.1"
|
||||
|
||||
dot-json@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dot-json/-/dot-json-1.2.2.tgz#7d35abece4aa22aa75a761388953f98495401bcc"
|
||||
@@ -1013,6 +1090,11 @@ emoji-regex@^8.0.0:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
entities@^4.2.0, entities@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656"
|
||||
integrity sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
@@ -1191,6 +1273,16 @@ html-escaper@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
||||
|
||||
htmlparser2@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010"
|
||||
integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
entities "^4.3.0"
|
||||
|
||||
human-signals@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||
@@ -1838,6 +1930,13 @@ npm-run-path@^4.0.1:
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
@@ -1881,6 +1980,21 @@ parse-json@^5.2.0:
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
|
||||
integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
|
||||
dependencies:
|
||||
domhandler "^5.0.2"
|
||||
parse5 "^7.0.0"
|
||||
|
||||
parse5@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a"
|
||||
integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==
|
||||
dependencies:
|
||||
entities "^4.3.0"
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
@@ -2193,6 +2307,11 @@ ts-jest@^28.0.3:
|
||||
semver "7.x"
|
||||
yargs-parser "^20.x"
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
type-detect@4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
|
Reference in New Issue
Block a user