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://lingva.ml/)
|
||||
[](./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
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { IconButton, useColorMode } from "@chakra-ui/react";
|
||||
import { SunIcon, MoonIcon } from "@chakra-ui/icons";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
type Props = {
|
||||
[key: string]: any
|
||||
@@ -8,6 +9,7 @@ type Props = {
|
||||
|
||||
const ColorModeToggler: FC<Props> = (props) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
useHotkeys("ctrl+shift+l, command+shift+l", toggleColorMode, [toggleColorMode]);
|
||||
return (
|
||||
<IconButton
|
||||
aria-label="Toggle color mode"
|
||||
|
||||
@@ -102,12 +102,25 @@ it("language switching button is disabled on 'auto', but enables when other", ()
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
cy.findByRole("combobox", { name: /target language/i })
|
||||
.as("target")
|
||||
.should("have.value", "eo")
|
||||
.get("@source")
|
||||
.should("have.value", "en")
|
||||
.url()
|
||||
.should("not.include", "/en")
|
||||
.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", () => {
|
||||
@@ -148,4 +161,9 @@ it("skips to main & toggles color mode", () => {
|
||||
.click()
|
||||
.get("body")
|
||||
.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({
|
||||
pwa: {
|
||||
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": {
|
||||
"@chakra-ui/icons": "^1.0.6",
|
||||
"@chakra-ui/react": "^1.3.4",
|
||||
"@chakra-ui/react": "^1.6.0",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@emotion/styled": "^11.1.5",
|
||||
"apollo-server-micro": "^2.22.1",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"framer-motion": "^3.10.3",
|
||||
"graphql": "^15.5.0",
|
||||
"next": "10.0.8",
|
||||
"next-pwa": "^5.0.6",
|
||||
"next": "10.2.0",
|
||||
"next-pwa": "^5.2.16",
|
||||
"nextjs-cors": "^1.0.4",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hotkeys-hook": "^3.3.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"user-agents": "^1.0.597"
|
||||
},
|
||||
@@ -32,7 +33,7 @@
|
||||
"@testing-library/cypress": "^7.0.4",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@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/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.33",
|
||||
@@ -42,7 +43,7 @@
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"apollo-server-testing": "^2.22.1",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"cypress": "^6.6.0",
|
||||
"cypress": "^7.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-cypress": "^2.11.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import Router from "next/router";
|
||||
import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react";
|
||||
import { FaExchangeAlt } from "react-icons/fa";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Layout, LangSelect, TranslationArea } from "../components";
|
||||
import { useToastOnLoad } from "../hooks";
|
||||
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);
|
||||
}, []);
|
||||
|
||||
const { sourceLangs, targetLangs } = retrieveFiltered(source, target);
|
||||
const { sourceLangs, targetLangs } = retrieveFiltered();
|
||||
const { source: transLang, target: queryLang } = replaceBoth("exception", { source: target, target: source });
|
||||
|
||||
useToastOnLoad({
|
||||
@@ -71,6 +72,12 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
||||
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 (
|
||||
<Layout home={home}>
|
||||
<VStack px={[8, null, 24, 40]} w="full">
|
||||
@@ -88,7 +95,7 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
||||
colorScheme="lingva"
|
||||
variant="ghost"
|
||||
onClick={() => dispatch({ type: Actions.SWITCH_LANGS })}
|
||||
isDisabled={source === "auto"}
|
||||
isDisabled={!canSwitch}
|
||||
/>
|
||||
<LangSelect
|
||||
id="target"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import faker from "faker";
|
||||
import { replaceBoth, retrieveFiltered, CheckType, LangType } from "../../utils/language";
|
||||
import { languages, exceptions, mappings } from "../../utils/languages.json";
|
||||
|
||||
@@ -36,16 +35,13 @@ describe("replaceBoth", () => {
|
||||
});
|
||||
|
||||
describe("retrieveFiltered", () => {
|
||||
const filteredEntries = (langType: LangType, current: string) => (
|
||||
Object.entries(languages).filter(([code]) => !Object.keys(exceptions[langType]).includes(code) && code !== current)
|
||||
const filteredEntries = (langType: LangType) => (
|
||||
Object.entries(languages).filter(([code]) => !Object.keys(exceptions[langType]).includes(code))
|
||||
);
|
||||
|
||||
it("filters by exceptions & by opposite values", () => {
|
||||
const source = faker.random.locale();
|
||||
const target = faker.random.locale();
|
||||
|
||||
const { sourceLangs, targetLangs } = retrieveFiltered(source, target);
|
||||
expect(sourceLangs).toStrictEqual(filteredEntries("source", target));
|
||||
expect(targetLangs).toStrictEqual(filteredEntries("target", source));
|
||||
it("filters by exceptions", () => {
|
||||
const { sourceLangs, targetLangs } = retrieveFiltered();
|
||||
expect(sourceLangs).toStrictEqual(filteredEntries("source"));
|
||||
expect(targetLangs).toStrictEqual(filteredEntries("target"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,24 @@ it("changes all fields", () => {
|
||||
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", () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { languages, exceptions, mappings } from "./languages.json";
|
||||
import languagesJson from "./languages.json";
|
||||
const { languages, exceptions, mappings } = languagesJson;
|
||||
|
||||
const checkTypes = {
|
||||
exception: exceptions,
|
||||
@@ -32,15 +33,10 @@ export function replaceBoth(
|
||||
return { source, target };
|
||||
}
|
||||
|
||||
export function retrieveFiltered(source: string, target: string) {
|
||||
const current = {
|
||||
source: target,
|
||||
target: source
|
||||
};
|
||||
export function retrieveFiltered() {
|
||||
const [sourceLangs, targetLangs] = langTypes.map(type => (
|
||||
Object.entries(languages).filter(([code]) => (
|
||||
!Object.keys(exceptions[type]).includes(code)
|
||||
&& current[type] !== code
|
||||
))
|
||||
));
|
||||
return { sourceLangs, targetLangs };
|
||||
|
||||
@@ -33,17 +33,22 @@ type Action = {
|
||||
}
|
||||
|
||||
export default function reducer(state: State, action: Action) {
|
||||
const { source, target } = replaceBoth("exception", {
|
||||
source: state.target,
|
||||
target: state.source
|
||||
});
|
||||
|
||||
switch (action.type) {
|
||||
case Actions.SET_FIELD:
|
||||
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 };
|
||||
case Actions.SET_ALL:
|
||||
return { ...state, ...action.payload.state };
|
||||
case Actions.SWITCH_LANGS:
|
||||
const { source, target } = replaceBoth("exception", {
|
||||
source: state.target,
|
||||
target: state.source
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
source: source !== target
|
||||
|
||||
Reference in New Issue
Block a user