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:
@@ -5,6 +5,7 @@
|
|||||||
[](https://travis-ci.com/TheDavidDelta/lingva-translate)
|
[](https://travis-ci.com/TheDavidDelta/lingva-translate)
|
||||||
[](https://lingva.ml/)
|
[](https://lingva.ml/)
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
|
[](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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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=()"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user