Add language endpoint to developer APIs (#41)
* Added languages endpoint and its tests
This commit is contained in:
56
README.md
56
README.md
@@ -58,16 +58,33 @@ Nearly all the *Lingva* instances should supply a pair of public developer APIs:
|
|||||||
+ GET `/api/v1/:source/:target/:query`
|
+ GET `/api/v1/:source/:target/:query`
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
translation?: string,
|
translation: string
|
||||||
errorMsg?: string
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
+ GET `/api/v1/audio/:lang/:query`
|
+ GET `/api/v1/audio/:lang/:query`
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
audio?: number[],
|
audio: number[]
|
||||||
errorMsg?: string
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
+ GET `/api/v1/languages/?:(source|target)`
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
languages: [
|
||||||
|
{
|
||||||
|
code: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, every endpoint can return an error message with the following structure instead.
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
error: string
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -78,20 +95,33 @@ Nearly all the *Lingva* instances should supply a pair of public developer APIs:
|
|||||||
query {
|
query {
|
||||||
translation(source: String target: String query: String!) {
|
translation(source: String target: String query: String!) {
|
||||||
source: {
|
source: {
|
||||||
lang: String!
|
lang: {
|
||||||
text: String
|
code: String!
|
||||||
audio: [Int]
|
name: String!
|
||||||
|
}
|
||||||
|
text: String!
|
||||||
|
audio: [Int]!
|
||||||
}
|
}
|
||||||
target: {
|
target: {
|
||||||
lang: String!
|
lang: {
|
||||||
text: String
|
code: String!
|
||||||
audio: [Int]
|
name: String!
|
||||||
|
}
|
||||||
|
text: String!
|
||||||
|
audio: [Int]!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audio(lang: String! query: String!) {
|
audio(lang: String! query: String!) {
|
||||||
lang: String!
|
lang: {
|
||||||
text: String
|
code: String!
|
||||||
audio: [Int]
|
name: String!
|
||||||
|
}
|
||||||
|
text: String!
|
||||||
|
audio: [Int]!
|
||||||
|
}
|
||||||
|
languages(type: SOURCE|TARGET) {
|
||||||
|
code: String!
|
||||||
|
name: String!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useHotkeys } from "react-hotkeys-hook";
|
|||||||
import { CustomHead, LangSelect, TranslationArea } from "@components";
|
import { CustomHead, LangSelect, TranslationArea } from "@components";
|
||||||
import { useToastOnLoad } from "@hooks";
|
import { useToastOnLoad } from "@hooks";
|
||||||
import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate";
|
import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate";
|
||||||
import { retrieveFiltered, replaceBoth } from "@utils/language";
|
import { retrieveFromType, replaceBoth } from "@utils/language";
|
||||||
import langReducer, { Actions, initialState } from "@utils/reducer";
|
import langReducer, { Actions, initialState } from "@utils/reducer";
|
||||||
|
|
||||||
const AutoTranslateButton = dynamic(() => import("@components/AutoTranslateButton"), { ssr: false });
|
const AutoTranslateButton = dynamic(() => import("@components/AutoTranslateButton"), { ssr: false });
|
||||||
@@ -66,7 +66,8 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
|||||||
return () => Router.events.off("beforeHistoryChange", handler);
|
return () => Router.events.off("beforeHistoryChange", handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { sourceLangs, targetLangs } = retrieveFiltered();
|
const sourceLangs = retrieveFromType("source");
|
||||||
|
const targetLangs = retrieveFromType("target");
|
||||||
const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source });
|
const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source });
|
||||||
|
|
||||||
useToastOnLoad({
|
useToastOnLoad({
|
||||||
@@ -191,7 +192,9 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
|
|||||||
|
|
||||||
const [sourceAudio, targetAudio] = await Promise.all([
|
const [sourceAudio, targetAudio] = await Promise.all([
|
||||||
textToSpeechScrape(source, query),
|
textToSpeechScrape(source, query),
|
||||||
textToSpeechScrape(target, textScrape.translationRes)
|
"translationRes" in textScrape
|
||||||
|
? textToSpeechScrape(target, textScrape.translationRes)
|
||||||
|
: null
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -205,7 +208,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
|
|||||||
source, target, query
|
source, target, query
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
revalidate: !textScrape.errorMsg
|
revalidate: !("errorMsg" in textScrape)
|
||||||
? 2 * 30 * 24 * 60 * 60 // 2 months
|
? 2 * 30 * 24 * 60 * 60 // 2 months
|
||||||
: 1
|
: 1
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import { ApolloServer, gql, IResolvers } from "apollo-server-micro";
|
import { ApolloServer, gql, IResolvers, ApolloError, UserInputError } from "apollo-server-micro";
|
||||||
import { NextApiHandler } from "next";
|
import { NextApiHandler } from "next";
|
||||||
import NextCors from "nextjs-cors";
|
import NextCors from "nextjs-cors";
|
||||||
import { googleScrape, textToSpeechScrape } from "@utils/translate";
|
import { googleScrape, textToSpeechScrape } from "@utils/translate";
|
||||||
|
import { retrieveFromType, getName } from "@utils/language";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
|
enum LangType {
|
||||||
|
SOURCE,
|
||||||
|
TARGET
|
||||||
|
}
|
||||||
type Query {
|
type Query {
|
||||||
translation(source: String="auto" target: String="en" query: String!): Translation!
|
translation(source: String="auto" target: String="en" query: String!): Translation!
|
||||||
audio(lang: String! query: String!): Entry!
|
audio(lang: String! query: String!): Entry!
|
||||||
|
languages(type: LangType): [Language]!
|
||||||
}
|
}
|
||||||
type Translation {
|
type Translation {
|
||||||
source: Entry!
|
source: Entry!
|
||||||
target: Entry!
|
target: Entry!
|
||||||
}
|
}
|
||||||
type Entry {
|
type Entry {
|
||||||
lang: String!
|
lang: Language!
|
||||||
text: String
|
text: String!
|
||||||
audio: [Int]
|
audio: [Int]!
|
||||||
|
}
|
||||||
|
type Language {
|
||||||
|
code: String!
|
||||||
|
name: String!
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -25,35 +35,65 @@ export const resolvers: IResolvers = {
|
|||||||
const { source, target, query } = args;
|
const { source, target, query } = args;
|
||||||
return {
|
return {
|
||||||
source: {
|
source: {
|
||||||
lang: source,
|
lang: {
|
||||||
|
code: source
|
||||||
|
},
|
||||||
text: query
|
text: query
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
lang: target
|
lang: {
|
||||||
|
code: target
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
audio(_, args) {
|
audio(_, args) {
|
||||||
|
const { lang, query } = args;
|
||||||
return {
|
return {
|
||||||
lang: args.lang,
|
lang: {
|
||||||
text: args.query
|
code: lang
|
||||||
|
},
|
||||||
|
text: query
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
languages(_, args) {
|
||||||
|
const { type } = args;
|
||||||
|
const langEntries = retrieveFromType(type?.toLocaleLowerCase());
|
||||||
|
return langEntries.map(([code, name]) => ({ code, name }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Translation: {
|
Translation: {
|
||||||
async target(parent) {
|
async target(parent) {
|
||||||
const { source, target } = parent;
|
const { source, target } = parent;
|
||||||
const { translationRes } = await googleScrape(source.lang, target.lang, source.text);
|
const textScrape = await googleScrape(source.lang.code, target.lang.code, source.text);
|
||||||
|
|
||||||
|
if ("errorMsg" in textScrape)
|
||||||
|
throw new ApolloError(textScrape.errorMsg);
|
||||||
return {
|
return {
|
||||||
lang: target.lang,
|
lang: target.lang,
|
||||||
text: translationRes
|
text: textScrape.translationRes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Entry: {
|
Entry: {
|
||||||
async audio(parent) {
|
async audio(parent) {
|
||||||
const { lang, text } = parent;
|
const { lang, text } = parent;
|
||||||
return await textToSpeechScrape(lang, text);
|
const audio = await textToSpeechScrape(lang.code, text);
|
||||||
|
if (!audio)
|
||||||
|
throw new ApolloError("An error occurred while retrieving the audio");
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Language: {
|
||||||
|
name(parent) {
|
||||||
|
const { code, name } = parent;
|
||||||
|
if (name)
|
||||||
|
return name;
|
||||||
|
|
||||||
|
const newName = getName(code);
|
||||||
|
if (!newName)
|
||||||
|
throw new UserInputError("Invalid language code");
|
||||||
|
return newName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import NextCors from "nextjs-cors";
|
|||||||
import { googleScrape, textToSpeechScrape } from "@utils/translate";
|
import { googleScrape, textToSpeechScrape } from "@utils/translate";
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
translation?: string,
|
translation: string,
|
||||||
audio?: number[],
|
} | {
|
||||||
error?: string
|
audio: number[]
|
||||||
|
} | {
|
||||||
|
error: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const methods = ["GET"];
|
const methods = ["GET"];
|
||||||
@@ -39,11 +41,11 @@ const handler: NextApiHandler<Data> = async (req, res) => {
|
|||||||
: res.status(500).json({ error: "An error occurred while retrieving the audio" });
|
: res.status(500).json({ error: "An error occurred while retrieving the audio" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { translationRes, errorMsg } = await googleScrape(source, target, query);
|
const textScrape = await googleScrape(source, target, query);
|
||||||
|
|
||||||
if (errorMsg)
|
if ("errorMsg" in textScrape)
|
||||||
return res.status(500).json({ error: errorMsg });
|
return res.status(500).json({ error: textScrape.errorMsg });
|
||||||
res.status(200).json({ translation: translationRes });
|
res.status(200).json({ translation: textScrape.translationRes });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
47
pages/api/v1/languages/[[...slug]].ts
Normal file
47
pages/api/v1/languages/[[...slug]].ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { NextApiHandler } from "next";
|
||||||
|
import NextCors from "nextjs-cors";
|
||||||
|
import { retrieveFromType } from "@utils/language";
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
languages: {
|
||||||
|
code: string,
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
} | {
|
||||||
|
error: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = ["GET"];
|
||||||
|
|
||||||
|
const handler: NextApiHandler<Data> = async (req, res) => {
|
||||||
|
await NextCors(req, res, {
|
||||||
|
methods,
|
||||||
|
origin: "*",
|
||||||
|
preflightContinue: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
query: { slug },
|
||||||
|
method
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
if (slug && Array.isArray(slug) && slug.length > 1)
|
||||||
|
return res.status(404).json({ error: "Not Found" });
|
||||||
|
|
||||||
|
if (!method || !methods.includes(method)) {
|
||||||
|
res.setHeader("Allow", methods);
|
||||||
|
return res.status(405).json({ error: "Method Not Allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = slug?.[0];
|
||||||
|
|
||||||
|
if (type !== undefined && type !== "source" && type !== "target")
|
||||||
|
return res.status(400).json({ error: "Type should be 'source', 'target' or empty" });
|
||||||
|
|
||||||
|
const langEntries = retrieveFromType(type);
|
||||||
|
const languages = langEntries.map(([code, name]) => ({ code, name }));
|
||||||
|
|
||||||
|
res.status(200).json({ languages });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -58,7 +58,9 @@ it("returns audio triggering fetch", async () => {
|
|||||||
query: `
|
query: `
|
||||||
query($lang: String! $text: String!) {
|
query($lang: String! $text: String!) {
|
||||||
audio(lang: $lang query: $text) {
|
audio(lang: $lang query: $text) {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
}
|
||||||
text
|
text
|
||||||
audio
|
audio
|
||||||
}
|
}
|
||||||
@@ -66,15 +68,15 @@ it("returns audio triggering fetch", async () => {
|
|||||||
`,
|
`,
|
||||||
variables: { lang, text }
|
variables: { lang, text }
|
||||||
});
|
});
|
||||||
expect(data).toMatchObject({ audio: { lang, text, audio: expect.any(Array) } });
|
expect(data).toMatchObject({ audio: { lang: { code: lang }, text, audio: expect.any(Array) } });
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null on translation error", async () => {
|
it("returns null and throws on translation error", async () => {
|
||||||
const text = faker.random.words();
|
const text = faker.random.words();
|
||||||
fetchMock.mockRejectOnce();
|
fetchMock.mockRejectOnce();
|
||||||
|
|
||||||
const { data } = await query({
|
const { data, errors } = await query({
|
||||||
query: `
|
query: `
|
||||||
query($text: String!) {
|
query($text: String!) {
|
||||||
translation(query: $text) {
|
translation(query: $text) {
|
||||||
@@ -86,15 +88,16 @@ it("returns null on translation error", async () => {
|
|||||||
`,
|
`,
|
||||||
variables: { text }
|
variables: { text }
|
||||||
});
|
});
|
||||||
expect(data).toMatchObject({ translation: { target: { text: null } } });
|
expect(data).toBeNull();
|
||||||
|
expect(errors).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null on audio error", async () => {
|
it("returns null and throws on audio error", async () => {
|
||||||
const lang = faker.random.locale();
|
const lang = faker.random.locale();
|
||||||
const text = faker.random.words();
|
const text = faker.random.words();
|
||||||
fetchMock.mockRejectOnce();
|
fetchMock.mockRejectOnce();
|
||||||
|
|
||||||
const { data } = await query({
|
const { data, errors } = await query({
|
||||||
query: `
|
query: `
|
||||||
query($lang: String! $text: String!) {
|
query($lang: String! $text: String!) {
|
||||||
audio(lang: $lang query: $text) {
|
audio(lang: $lang query: $text) {
|
||||||
@@ -104,29 +107,52 @@ it("returns null on audio error", async () => {
|
|||||||
`,
|
`,
|
||||||
variables: { lang, text }
|
variables: { lang, text }
|
||||||
});
|
});
|
||||||
expect(data).toMatchObject({ audio: { audio: null } });
|
expect(data).toBeNull();
|
||||||
|
expect(errors).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps a default value for both source and target languages", async () => {
|
it("keeps a default value for both source and target languages", async () => {
|
||||||
const text = faker.random.words();
|
const text = faker.random.words();
|
||||||
fetchMock.mockRejectOnce();
|
const translation = faker.random.words();
|
||||||
|
resolveFetchWith(htmlRes(translation));
|
||||||
|
|
||||||
const { data } = await query({
|
const { data } = await query({
|
||||||
query: `
|
query: `
|
||||||
query($text: String!) {
|
query($text: String!) {
|
||||||
translation(query: $text) {
|
translation(query: $text) {
|
||||||
source {
|
source {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
target {
|
target {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { text }
|
variables: { text }
|
||||||
});
|
});
|
||||||
expect(data).toMatchObject({ translation: { source: { lang: "auto" }, target: { lang: "en" } } });
|
expect(data).toMatchObject({
|
||||||
|
translation: {
|
||||||
|
source: {
|
||||||
|
lang: {
|
||||||
|
code: "auto",
|
||||||
|
name: "Detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
lang: {
|
||||||
|
code: "en",
|
||||||
|
name: "English"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error on empty query in translation", async () => {
|
it("throws error on empty query in translation", async () => {
|
||||||
@@ -135,10 +161,14 @@ it("throws error on empty query in translation", async () => {
|
|||||||
query {
|
query {
|
||||||
translation {
|
translation {
|
||||||
source {
|
source {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
target {
|
target {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +185,9 @@ it("throws error on empty lang or query in audio", async () => {
|
|||||||
query: `
|
query: `
|
||||||
query($lang: String!) {
|
query($lang: String!) {
|
||||||
audio(lang: $lang) {
|
audio(lang: $lang) {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +200,9 @@ it("throws error on empty lang or query in audio", async () => {
|
|||||||
query: `
|
query: `
|
||||||
query($text: String!) {
|
query($text: String!) {
|
||||||
audio(query: $text) {
|
audio(query: $text) {
|
||||||
lang
|
lang {
|
||||||
|
code
|
||||||
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,3 +211,44 @@ it("throws error on empty lang or query in audio", async () => {
|
|||||||
});
|
});
|
||||||
expect(langErrors).toBeTruthy();
|
expect(langErrors).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns languages on empty type", async () => {
|
||||||
|
const { data } = await query({
|
||||||
|
query: `
|
||||||
|
query {
|
||||||
|
languages {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(data).toMatchObject({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns languages on 'source' type", async () => {
|
||||||
|
const { data } = await query({
|
||||||
|
query: `
|
||||||
|
query($type: LangType!) {
|
||||||
|
languages(type: $type) {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { type: "SOURCE" }
|
||||||
|
});
|
||||||
|
expect(data).toMatchObject({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns languages on 'target' type", async () => {
|
||||||
|
const { data } = await query({
|
||||||
|
query: `
|
||||||
|
query($type: LangType!) {
|
||||||
|
languages(type: $type) {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { type: "TARGET" }
|
||||||
|
});
|
||||||
|
expect(data).toMatchObject({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
|
|||||||
65
tests/pages/api/v1/languages/[[...slug]].test.ts
Normal file
65
tests/pages/api/v1/languages/[[...slug]].test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import httpMocks from "node-mocks-http";
|
||||||
|
import handler from "@pages/api/v1/languages/[[...slug]]";
|
||||||
|
|
||||||
|
it("returns 404 on >1 params", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "GET",
|
||||||
|
query: { slug: ["one", "two"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 405 on forbidden method", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "POST",
|
||||||
|
query: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 on wrong param", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "GET",
|
||||||
|
query: { slug: ["other"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 200 on empty param", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "GET",
|
||||||
|
query: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res._getJSONData()).toStrictEqual({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 200 on 'source' param", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "GET",
|
||||||
|
query: { slug: ["source"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res._getJSONData()).toStrictEqual({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 200 on 'target' param", async () => {
|
||||||
|
const { req, res } = httpMocks.createMocks<any, any>({
|
||||||
|
method: "GET",
|
||||||
|
query: { slug: ["target"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res._getJSONData()).toStrictEqual({ languages: expect.any(Array) });
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { replaceBoth, retrieveFiltered, CheckType, LangType } from "@utils/language";
|
import faker from "faker";
|
||||||
|
import { replaceBoth, retrieveFromType, getName, CheckType, LangType } from "@utils/language";
|
||||||
import { languages, exceptions, mappings } from "@utils/languages.json";
|
import { languages, exceptions, mappings } from "@utils/languages.json";
|
||||||
|
|
||||||
describe("replaceBoth", () => {
|
describe("replaceBoth", () => {
|
||||||
@@ -34,14 +35,34 @@ describe("replaceBoth", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("retrieveFiltered", () => {
|
describe("retrieveFromType", () => {
|
||||||
const filteredEntries = (langType: LangType) => (
|
const checkExceptions = (langType: LangType) => (
|
||||||
Object.entries(languages).filter(([code]) => !Object.keys(exceptions[langType]).includes(code))
|
retrieveFromType(langType).forEach(([code]) => !Object.keys(exceptions).includes(code))
|
||||||
);
|
);
|
||||||
|
|
||||||
it("filters by exceptions", () => {
|
it("returns full list on empty type", () => {
|
||||||
const { sourceLangs, targetLangs } = retrieveFiltered();
|
expect(retrieveFromType()).toStrictEqual(Object.entries(languages));
|
||||||
expect(sourceLangs).toStrictEqual(filteredEntries("source"));
|
});
|
||||||
expect(targetLangs).toStrictEqual(filteredEntries("target"));
|
|
||||||
|
it("filters source exceptions", () => {
|
||||||
|
checkExceptions("source");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters target exceptions", () => {
|
||||||
|
checkExceptions("target");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getName", () => {
|
||||||
|
it("returns name from valid code", () => {
|
||||||
|
const langEntries = Object.entries(languages);
|
||||||
|
const randomEntry = faker.random.arrayElement(langEntries);
|
||||||
|
const [code, name] = randomEntry;
|
||||||
|
expect(getName(code)).toEqual(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null on wrong code", () => {
|
||||||
|
const randomCode = faker.random.words();
|
||||||
|
expect(getName(randomCode)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ describe("googleScrape", () => {
|
|||||||
resolveFetchWith({ status });
|
resolveFetchWith({ status });
|
||||||
|
|
||||||
const res = await googleScrape(source, target, query);
|
const res = await googleScrape(source, target, query);
|
||||||
expect(res?.errorMsg).toMatch(/retrieving/);
|
expect("errorMsg" in res && res.errorMsg).toMatch(/retrieving/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns correct message on network error", async () => {
|
it("returns correct message on network error", async () => {
|
||||||
fetchMock.mockRejectOnce();
|
fetchMock.mockRejectOnce();
|
||||||
|
|
||||||
const res = await googleScrape(source, target, query);
|
const res = await googleScrape(source, target, query);
|
||||||
expect(res?.errorMsg).toMatch(/retrieving/);
|
expect("errorMsg" in res && res.errorMsg).toMatch(/retrieving/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns correct message on parsing wrong class", async () => {
|
it("returns correct message on parsing wrong class", async () => {
|
||||||
@@ -41,7 +41,7 @@ describe("googleScrape", () => {
|
|||||||
resolveFetchWith(html);
|
resolveFetchWith(html);
|
||||||
|
|
||||||
const res = await googleScrape(source, target, query);
|
const res = await googleScrape(source, target, query);
|
||||||
expect(res?.errorMsg).toMatch(/parsing/);
|
expect("errorMsg" in res && res.errorMsg).toMatch(/parsing/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,16 @@ export function replaceBoth(
|
|||||||
return { source, target };
|
return { source, target };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function retrieveFiltered() {
|
export function retrieveFromType(type?: LangType): [string, string][] {
|
||||||
const [sourceLangs, targetLangs] = langTypes.map(type => (
|
const langEntries = Object.entries(languages);
|
||||||
Object.entries(languages).filter(([code]) => (
|
|
||||||
!Object.keys(exceptions[type]).includes(code)
|
if (!type)
|
||||||
))
|
return langEntries;
|
||||||
|
return langEntries.filter(([code]) => (
|
||||||
|
!Object.keys(exceptions[type]).includes(code)
|
||||||
));
|
));
|
||||||
return { sourceLangs, targetLangs };
|
}
|
||||||
|
|
||||||
|
export function getName(code: string): string | null {
|
||||||
|
return isKeyOf(languages)(code) ? languages[code] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ export async function googleScrape(
|
|||||||
target: string,
|
target: string,
|
||||||
query: string
|
query: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
translationRes?: string,
|
translationRes: string
|
||||||
errorMsg?: string
|
} | {
|
||||||
|
errorMsg: string
|
||||||
}> {
|
}> {
|
||||||
const parsed = replaceBoth("mapping", { source, target });
|
const parsed = replaceBoth("mapping", { source, target });
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -56,10 +57,7 @@ export function extractSlug(slug: string[]): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function textToSpeechScrape(lang: string, text?: string) {
|
export async function textToSpeechScrape(lang: string, text: string) {
|
||||||
if (!text)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const { target: parsedLang } = replaceBoth("mapping", { source: "", target: lang });
|
const { target: parsedLang } = replaceBoth("mapping", { source: "", target: lang });
|
||||||
|
|
||||||
const lastSpace = text.lastIndexOf(" ", 200);
|
const lastSpace = text.lastIndexOf(" ", 200);
|
||||||
|
|||||||
Reference in New Issue
Block a user