Added hotkeys + updated dependencies & webpack5 + humanetech badge (#15)

* Added language & theme switch hotkeys

* Hotkeys testing

* Language switch when equal

* Shortcut keys changed

* Dependencies updated & upgraded to webpack5

* Added HumaneTech badge

* Disabled FLoC
This commit is contained in:
David
2021-04-30 23:11:26 +02:00
committed by GitHub
parent a80c177440
commit cf0b310918
11 changed files with 1511 additions and 1215 deletions

View File

@@ -5,6 +5,7 @@
[![Travis Build](https://travis-ci.com/TheDavidDelta/lingva-translate.svg?branch=main)](https://travis-ci.com/TheDavidDelta/lingva-translate) [![Travis Build](https://travis-ci.com/TheDavidDelta/lingva-translate.svg?branch=main)](https://travis-ci.com/TheDavidDelta/lingva-translate)
[![Vercel Status](https://img.shields.io/github/deployments/TheDavidDelta/lingva-translate/Production?label=vercel&logo=vercel&color=f5f5f5)](https://lingva.ml/) [![Vercel Status](https://img.shields.io/github/deployments/TheDavidDelta/lingva-translate/Production?label=vercel&logo=vercel&color=f5f5f5)](https://lingva.ml/)
[![License](https://img.shields.io/github/license/TheDavidDelta/lingva-translate)](./LICENSE) [![License](https://img.shields.io/github/license/TheDavidDelta/lingva-translate)](./LICENSE)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
Alternative front-end for Google Translate, serving as a Free and Open Source translator with over a hundred languages available Alternative front-end for Google Translate, serving as a Free and Open Source translator with over a hundred languages available

View File

@@ -1,6 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { IconButton, useColorMode } from "@chakra-ui/react"; import { IconButton, useColorMode } from "@chakra-ui/react";
import { SunIcon, MoonIcon } from "@chakra-ui/icons"; import { SunIcon, MoonIcon } from "@chakra-ui/icons";
import { useHotkeys } from "react-hotkeys-hook";
type Props = { type Props = {
[key: string]: any [key: string]: any
@@ -8,6 +9,7 @@ type Props = {
const ColorModeToggler: FC<Props> = (props) => { const ColorModeToggler: FC<Props> = (props) => {
const { colorMode, toggleColorMode } = useColorMode(); const { colorMode, toggleColorMode } = useColorMode();
useHotkeys("ctrl+shift+l, command+shift+l", toggleColorMode, [toggleColorMode]);
return ( return (
<IconButton <IconButton
aria-label="Toggle color mode" aria-label="Toggle color mode"

View File

@@ -102,12 +102,25 @@ it("language switching button is disabled on 'auto', but enables when other", ()
.should("be.enabled") .should("be.enabled")
.click(); .click();
cy.findByRole("combobox", { name: /target language/i }) cy.findByRole("combobox", { name: /target language/i })
.as("target")
.should("have.value", "eo") .should("have.value", "eo")
.get("@source") .get("@source")
.should("have.value", "en") .should("have.value", "en")
.url() .url()
.should("not.include", "/en") .should("not.include", "/en")
.should("not.include", "/eo"); .should("not.include", "/eo");
cy.get("body")
.type("{ctrl}{shift}s")
.get("@source")
.should("have.value", "eo")
.get("@target")
.should("have.value", "en")
.get("body")
.type("{ctrl}{shift}f")
.get("@source")
.should("have.value", "en")
.get("@target")
.should("have.value", "eo");
}); });
it("loads & plays audio correctly", () => { it("loads & plays audio correctly", () => {
@@ -148,4 +161,9 @@ it("skips to main & toggles color mode", () => {
.click() .click()
.get("body") .get("body")
.should("have.css", "background-color", white); .should("have.css", "background-color", white);
cy.get("body")
.type("{ctrl}{shift}l")
.should("not.have.css", "background-color", white)
.type("{ctrl}{shift}l")
.should("have.css", "background-color", white);
}); });

View File

@@ -3,5 +3,21 @@ const withPWA = require("next-pwa");
module.exports = withPWA({ module.exports = withPWA({
pwa: { pwa: {
dest: "public" dest: "public"
},
future: {
webpack5: true
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Permissions-Policy",
value: "interest-cohort=()"
}
]
}
]
} }
}); });

View File

@@ -13,18 +13,19 @@
}, },
"dependencies": { "dependencies": {
"@chakra-ui/icons": "^1.0.6", "@chakra-ui/icons": "^1.0.6",
"@chakra-ui/react": "^1.3.4", "@chakra-ui/react": "^1.6.0",
"@emotion/react": "^11.1.5", "@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5", "@emotion/styled": "^11.1.5",
"apollo-server-micro": "^2.22.1", "apollo-server-micro": "^2.22.1",
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"framer-motion": "^3.10.3", "framer-motion": "^3.10.3",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"next": "10.0.8", "next": "10.2.0",
"next-pwa": "^5.0.6", "next-pwa": "^5.2.16",
"nextjs-cors": "^1.0.4", "nextjs-cors": "^1.0.4",
"react": "17.0.1", "react": "17.0.2",
"react-dom": "17.0.1", "react-dom": "17.0.2",
"react-hotkeys-hook": "^3.3.0",
"react-icons": "^4.2.0", "react-icons": "^4.2.0",
"user-agents": "^1.0.597" "user-agents": "^1.0.597"
}, },
@@ -32,7 +33,7 @@
"@testing-library/cypress": "^7.0.4", "@testing-library/cypress": "^7.0.4",
"@testing-library/jest-dom": "^5.11.9", "@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5", "@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^13.1.8",
"@types/faker": "^5.1.7", "@types/faker": "^5.1.7",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/node": "^14.14.33", "@types/node": "^14.14.33",
@@ -42,7 +43,7 @@
"@typescript-eslint/parser": "^4.0.0", "@typescript-eslint/parser": "^4.0.0",
"apollo-server-testing": "^2.22.1", "apollo-server-testing": "^2.22.1",
"babel-eslint": "^10.0.0", "babel-eslint": "^10.0.0",
"cypress": "^6.6.0", "cypress": "^7.2.0",
"eslint": "^7.5.0", "eslint": "^7.5.0",
"eslint-config-react-app": "^6.0.0", "eslint-config-react-app": "^6.0.0",
"eslint-plugin-cypress": "^2.11.2", "eslint-plugin-cypress": "^2.11.2",

View File

@@ -3,6 +3,7 @@ import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import Router from "next/router"; import Router from "next/router";
import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react"; import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react";
import { FaExchangeAlt } from "react-icons/fa"; import { FaExchangeAlt } from "react-icons/fa";
import { useHotkeys } from "react-hotkeys-hook";
import { Layout, LangSelect, TranslationArea } from "../components"; import { Layout, 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";
@@ -61,7 +62,7 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
return () => Router.events.off("beforeHistoryChange", handler); return () => Router.events.off("beforeHistoryChange", handler);
}, []); }, []);
const { sourceLangs, targetLangs } = retrieveFiltered(source, target); const { sourceLangs, targetLangs } = retrieveFiltered();
const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source }); const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source });
useToastOnLoad({ useToastOnLoad({
@@ -71,6 +72,12 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
updateDeps: initial updateDeps: initial
}); });
const canSwitch = source !== "auto" && !isLoading;
useHotkeys("ctrl+shift+s, command+shift+s, ctrl+shift+f, command+shift+f", () => (
canSwitch && dispatch({ type: Actions.SWITCH_LANGS })
), [canSwitch]);
return ( return (
<Layout home={home}> <Layout home={home}>
<VStack px={[8, null, 24, 40]} w="full"> <VStack px={[8, null, 24, 40]} w="full">
@@ -88,7 +95,7 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
colorScheme="lingva" colorScheme="lingva"
variant="ghost" variant="ghost"
onClick={() => dispatch({ type: Actions.SWITCH_LANGS })} onClick={() => dispatch({ type: Actions.SWITCH_LANGS })}
isDisabled={source === "auto"} isDisabled={!canSwitch}
/> />
<LangSelect <LangSelect
id="target" id="target"

View File

@@ -1,4 +1,3 @@
import faker from "faker";
import { replaceBoth, retrieveFiltered, CheckType, LangType } from "../../utils/language"; import { replaceBoth, retrieveFiltered, CheckType, LangType } from "../../utils/language";
import { languages, exceptions, mappings } from "../../utils/languages.json"; import { languages, exceptions, mappings } from "../../utils/languages.json";
@@ -36,16 +35,13 @@ describe("replaceBoth", () => {
}); });
describe("retrieveFiltered", () => { describe("retrieveFiltered", () => {
const filteredEntries = (langType: LangType, current: string) => ( const filteredEntries = (langType: LangType) => (
Object.entries(languages).filter(([code]) => !Object.keys(exceptions[langType]).includes(code) && code !== current) Object.entries(languages).filter(([code]) => !Object.keys(exceptions[langType]).includes(code))
); );
it("filters by exceptions & by opposite values", () => { it("filters by exceptions", () => {
const source = faker.random.locale(); const { sourceLangs, targetLangs } = retrieveFiltered();
const target = faker.random.locale(); expect(sourceLangs).toStrictEqual(filteredEntries("source"));
expect(targetLangs).toStrictEqual(filteredEntries("target"));
const { sourceLangs, targetLangs } = retrieveFiltered(source, target);
expect(sourceLangs).toStrictEqual(filteredEntries("source", target));
expect(targetLangs).toStrictEqual(filteredEntries("target", source));
}); });
}); });

View File

@@ -32,6 +32,24 @@ it("changes all fields", () => {
expect(res).toStrictEqual(state); expect(res).toStrictEqual(state);
}); });
it("switches target on source change", () => {
const state = {
...initialState,
source: "es",
target: "ca"
};
const res = langReducer(state, {
type: Actions.SET_FIELD,
payload: {
key: "source",
value: state.target
}
});
expect(res.source).toStrictEqual(state.target);
expect(res.target).toStrictEqual(state.source);
});
it("switches the languages & the translations", () => { it("switches the languages & the translations", () => {
const state = { const state = {
...initialState, ...initialState,

View File

@@ -1,4 +1,5 @@
import { languages, exceptions, mappings } from "./languages.json"; import languagesJson from "./languages.json";
const { languages, exceptions, mappings } = languagesJson;
const checkTypes = { const checkTypes = {
exception: exceptions, exception: exceptions,
@@ -32,15 +33,10 @@ export function replaceBoth(
return { source, target }; return { source, target };
} }
export function retrieveFiltered(source: string, target: string) { export function retrieveFiltered() {
const current = {
source: target,
target: source
};
const [sourceLangs, targetLangs] = langTypes.map(type => ( const [sourceLangs, targetLangs] = langTypes.map(type => (
Object.entries(languages).filter(([code]) => ( Object.entries(languages).filter(([code]) => (
!Object.keys(exceptions[type]).includes(code) !Object.keys(exceptions[type]).includes(code)
&& current[type] !== code
)) ))
)); ));
return { sourceLangs, targetLangs }; return { sourceLangs, targetLangs };

View File

@@ -33,17 +33,22 @@ type Action = {
} }
export default function reducer(state: State, action: Action) { export default function reducer(state: State, action: Action) {
const { source, target } = replaceBoth("exception", {
source: state.target,
target: state.source
});
switch (action.type) { switch (action.type) {
case Actions.SET_FIELD: case Actions.SET_FIELD:
const { key, value } = action.payload; const { key, value } = action.payload;
if (key === "source" && value === state.target)
return { ...state, [key]: value, target: target !== value ? target : "eo" };
if (key === "target" && value === state.source)
return { ...state, [key]: value, source };
return { ...state, [key]: value }; return { ...state, [key]: value };
case Actions.SET_ALL: case Actions.SET_ALL:
return { ...state, ...action.payload.state }; return { ...state, ...action.payload.state };
case Actions.SWITCH_LANGS: case Actions.SWITCH_LANGS:
const { source, target } = replaceBoth("exception", {
source: state.target,
target: state.source
});
return { return {
...state, ...state,
source: source !== target source: source !== target

2606
yarn.lock

File diff suppressed because it is too large Load Diff