Default languages with env & langcodes typed (#94)

This commit is contained in:
David
2022-02-24 19:53:31 +01:00
committed by GitHub
parent 5e4db73a9f
commit 5856776785
15 changed files with 89 additions and 50 deletions

View File

@@ -23,4 +23,7 @@ ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
CMD NEXT_PUBLIC_SITE_DOMAIN=$site_domain DEFAULT_DARK_THEME=$dark_theme yarn build && yarn start
CMD NEXT_PUBLIC_SITE_DOMAIN=$site_domain DEFAULT_DARK_THEME=$dark_theme \
NEXT_PUBLIC_DEFAULT_SOURCE_LANG=$default_source_lang \
NEXT_PUBLIC_DEFAULT_TARGET_LANG=$default_target_lang \
yarn build && yarn start

View File

@@ -53,6 +53,8 @@ services:
environment:
- site_domain=lingva.ml
- dark_theme=false
- default_source_lang=auto
- default_target_lang=en
ports:
- 3000:3000
```
@@ -60,7 +62,7 @@ services:
#### Docker Run
```bash
docker run -p 3000:3000 -e site_domain=lingva.ml -e dark_theme=false thedaviddelta/lingva-translate:latest
docker run -p 3000:3000 -e site_domain=lingva.ml -e dark_theme=false -e default_source_lang=auto -e default_target_lang=en thedaviddelta/lingva-translate:latest
```
### Vercel

View File

@@ -9,7 +9,7 @@ import { useHotkeys } from "react-hotkeys-hook";
import { CustomHead, LangSelect, TranslationArea } from "@components";
import { useToastOnLoad } from "@hooks";
import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate";
import { retrieveFromType, replaceBoth } from "@utils/language";
import { retrieveFromType, replaceBoth, isValid } from "@utils/language";
import langReducer, { Actions, initialState } from "@utils/reducer";
import { localGetItem, localSetItem } from "@utils/storage";
@@ -46,18 +46,22 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
}, [isLoading, source, target, home, initial]);
useEffect(() => {
if (home)
if (home) {
const localSource = localGetItem("source");
const localTarget = localGetItem("target");
return dispatch({
type: Actions.SET_ALL,
payload: {
state: {
...initialState,
source: localGetItem("source") || initialState.source,
target: localGetItem("target") || initialState.target,
source: isValid(localSource) ? localSource : initialState.source,
target: isValid(localTarget) ? localTarget : initialState.target,
isLoading: false
}
}
});
}
if (!initial)
return;
@@ -218,6 +222,11 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
}
}
if (!isValid(source) || !isValid(target))
return {
notFound: true
};
const textScrape = await googleScrape(source, target, query);
const [sourceAudio, targetAudio] = await Promise.all([

View File

@@ -2,7 +2,7 @@ import { ApolloServer, gql, IResolvers, ApolloError, UserInputError } from "apol
import { NextApiHandler } from "next";
import NextCors from "nextjs-cors";
import { googleScrape, textToSpeechScrape } from "@utils/translate";
import { retrieveFromType, getName } from "@utils/language";
import { retrieveFromType, getName, isValid } from "@utils/language";
export const typeDefs = gql`
enum LangType {
@@ -33,6 +33,10 @@ export const resolvers: IResolvers = {
Query: {
translation(_, args) {
const { source, target, query } = args;
if (!isValid(source) || !isValid(target))
throw new UserInputError("Invalid language code");
return {
source: {
lang: {
@@ -49,6 +53,10 @@ export const resolvers: IResolvers = {
},
audio(_, args) {
const { lang, query } = args;
if (!isValid(lang))
throw new UserInputError("Invalid language code");
return {
lang: {
code: lang
@@ -87,13 +95,7 @@ export const resolvers: IResolvers = {
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;
return name || getName(code);
}
}
};

View File

@@ -1,6 +1,7 @@
import { NextApiHandler } from "next";
import NextCors from "nextjs-cors";
import { googleScrape, textToSpeechScrape } from "@utils/translate";
import { isValid } from "@utils/language";
type Data = {
translation: string,
@@ -34,6 +35,9 @@ const handler: NextApiHandler<Data> = async (req, res) => {
const [source, target, query] = slug;
if (!isValid(target))
return res.status(400).json({ error: "Invalid target language" });
if (source === "audio") {
const audio = await textToSpeechScrape(target, query);
return audio
@@ -41,6 +45,9 @@ const handler: NextApiHandler<Data> = async (req, res) => {
: res.status(500).json({ error: "An error occurred while retrieving the audio" });
}
if (!isValid(source))
return res.status(400).json({ error: "Invalid source language" });
const textScrape = await googleScrape(source, target, query);
if ("errorMsg" in textScrape)

View File

@@ -1,10 +1,10 @@
import { NextApiHandler } from "next";
import NextCors from "nextjs-cors";
import { retrieveFromType } from "@utils/language";
import { retrieveFromType, LangCode } from "@utils/language";
type Data = {
languages: {
code: string,
code: LangCode,
name: string
}[]
} | {

View File

@@ -12,8 +12,8 @@ beforeEach(() => {
});
describe("getStaticProps", () => {
const source = faker.random.locale();
const target = faker.random.locale();
const source = "es";
const target = "ca";
const query = faker.random.words();
it("returns home on empty params", async () => {

View File

@@ -50,7 +50,7 @@ it("returns translation triggering fetch", async () => {
});
it("returns audio triggering fetch", async () => {
const lang = faker.random.locale();
const lang = "es";
const text = faker.random.words();
resolveFetchWith({ status: 200 });
@@ -93,7 +93,7 @@ it("returns null and throws on translation error", async () => {
});
it("returns null and throws on audio error", async () => {
const lang = faker.random.locale();
const lang = "es";
const text = faker.random.words();
fetchMock.mockRejectOnce();
@@ -178,7 +178,7 @@ it("throws error on empty query in translation", async () => {
});
it("throws error on empty lang or query in audio", async () => {
const lang = faker.random.locale();
const lang = "es";
const text = faker.random.words();
const { errors: queryErrors } = await query({

View File

@@ -7,8 +7,8 @@ beforeEach(() => {
fetchMock.resetMocks();
});
const source = faker.random.locale();
const target = faker.random.locale();
const source = "es";
const target = "ca";
const query = faker.random.words();
const slug = [source, target, query];

View File

@@ -13,7 +13,7 @@ describe("replaceBoth", () => {
langType: LangType
) => (
Object.entries(checkObj[langType]).forEach(([code, replacement]) => {
const res = replaceBoth(checkType, { source: "", target: "", [langType]: code })
const res = replaceBoth(checkType, { source: "auto", target: "en", [langType]: code })
expect(res[langType]).toBe(replacement);
})
);

View File

@@ -17,13 +17,13 @@ it("changes a field value", () => {
it("changes all fields", () => {
const query = faker.random.words();
const state = {
source: faker.random.locale(),
target: faker.random.locale(),
source: "zh",
target: "zh_HANT",
query,
delayedQuery: query,
translation: faker.random.words(),
isLoading: faker.datatype.boolean()
};
} as const;
const res = langReducer(initialState, {
type: Actions.SET_ALL,
@@ -37,7 +37,7 @@ it("switches target on source change", () => {
...initialState,
source: "es",
target: "ca"
};
} as const;
const res = langReducer(state, {
type: Actions.SET_FIELD,
@@ -57,7 +57,7 @@ it("switches the languages & the translations", () => {
target: "ca",
query: faker.random.words(),
translation: faker.random.words()
};
} as const;
const res = langReducer(state, { type: Actions.SWITCH_LANGS });
expect(res).toStrictEqual({
@@ -75,7 +75,7 @@ it("resets the source while switching if they're the same", () => {
...initialState,
source: "eo",
target: "eo"
};
} as const;
const res = langReducer(state, { type: Actions.SWITCH_LANGS });
expect(res.source).toStrictEqual(initialState.source);

View File

@@ -2,8 +2,8 @@ import { htmlRes, resolveFetchWith } from "@tests/commonUtils";
import faker from "faker";
import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate";
const source = faker.random.locale();
const target = faker.random.locale();
const source = "es";
const target = "ca";
const query = faker.random.words();
describe("googleScrape", () => {

View File

@@ -1,6 +1,8 @@
import languagesJson from "./languages.json";
const { languages, exceptions, mappings } = languagesJson;
export type LangCode = keyof typeof languages;
const checkTypes = {
exception: exceptions,
mapping: mappings
@@ -20,10 +22,10 @@ const isKeyOf = <T extends object>(obj: T) => (key: keyof any): key is keyof T =
export function replaceBoth(
checkType: CheckType,
langs: {
[key in LangType]: string
[key in LangType]: LangCode
}
): {
[key in LangType]: string
[key in LangType]: LangCode
} {
const [source, target] = langTypes.map(langType => {
const object = checkTypes[checkType][langType];
@@ -33,8 +35,8 @@ export function replaceBoth(
return { source, target };
}
export function retrieveFromType(type?: LangType): [string, string][] {
const langEntries = Object.entries(languages);
export function retrieveFromType(type?: LangType) {
const langEntries = Object.entries(languages) as [LangCode, string][];
if (!type)
return langEntries;
@@ -43,6 +45,10 @@ export function retrieveFromType(type?: LangType): [string, string][] {
));
}
export function getName(code: string): string | null {
return isKeyOf(languages)(code) ? languages[code] : null;
export function isValid(code: string | null | undefined): code is LangCode {
return !!code && isKeyOf(languages)(code);
}
export function getName(code: string): string | null {
return isValid(code) ? languages[code] : null;
}

View File

@@ -1,16 +1,26 @@
import { replaceBoth } from "./language";
import { replaceBoth, isValid, LangCode } from "./language";
export const initialState = {
source: "auto",
target: "en",
const defaultSourceLang = process.env["NEXT_PUBLIC_DEFAULT_SOURCE_LANG"];
const defaultTargetLang = process.env["NEXT_PUBLIC_DEFAULT_TARGET_LANG"];
type State = {
source: LangCode,
target: LangCode,
query: string,
delayedQuery: string,
translation: string,
isLoading: boolean
}
export const initialState: State = {
source: isValid(defaultSourceLang) ? defaultSourceLang : "auto",
target: isValid(defaultTargetLang) ? defaultTargetLang : "en",
query: "",
delayedQuery: "",
translation: "",
isLoading: true
}
type State = typeof initialState;
export enum Actions {
SET_FIELD,
SET_ALL,
@@ -32,7 +42,7 @@ type Action = {
type: Actions.SWITCH_LANGS
}
export default function reducer(state: State, action: Action) {
export default function reducer(state: State, action: Action): State {
const { source, target } = replaceBoth("exception", {
source: state.target,
target: state.source

View File

@@ -1,10 +1,10 @@
import UserAgent from "user-agents";
import cheerio from "cheerio";
import { replaceBoth } from "./language";
import { replaceBoth, LangCode } from "./language";
export async function googleScrape(
source: string,
target: string,
source: LangCode,
target: LangCode,
query: string
): Promise<{
translationRes: string
@@ -57,8 +57,8 @@ export function extractSlug(slug: string[]): {
}
}
export async function textToSpeechScrape(lang: string, text: string) {
const { target: parsedLang } = replaceBoth("mapping", { source: "", target: lang });
export async function textToSpeechScrape(lang: LangCode, text: string) {
const { target: parsedLang } = replaceBoth("mapping", { source: "auto", target: lang });
const lastSpace = text.lastIndexOf(" ", 200);
const slicedText = text.slice(0, text.length > 200 && lastSpace !== -1 ? lastSpace : 200);