Default languages with env & langcodes typed (#94)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}[]
|
||||
} | {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user