From 5bc7693b21e319d4e73bc6ebff06ced8b2d58675 Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Fri, 12 Jan 2024 14:32:51 +0100 Subject: [PATCH] Add support for parsing of language from url param --- cypress/e2e/index.cy.js | 33 ++++++++++++ gatsby-browser.js | 8 ++- src/common.js | 21 ++++++++ src/components/Collection.jsx | 26 +--------- src/components/Concept.jsx | 18 +------ src/components/ConceptScheme.jsx | 18 +------ src/components/header.jsx | 16 ++---- src/hooks/getUserLanguage.js | 13 +++-- src/pages/index.js | 38 ++++++++++---- src/templates/App.jsx | 86 +++++++++++++++++++++++++++++--- test/App.test.jsx | 24 ++++++--- test/common.test.js | 16 ++++++ 12 files changed, 217 insertions(+), 100 deletions(-) diff --git a/cypress/e2e/index.cy.js b/cypress/e2e/index.cy.js index 807101f5..74a7bbbe 100644 --- a/cypress/e2e/index.cy.js +++ b/cypress/e2e/index.cy.js @@ -68,4 +68,37 @@ describe("Main Vocab Index page", () => { cy.go("back") cy.get(".conceptScheme > a").should("not.exist") }) + + it("German language is selected, when lang=de param is given in url", () => { + cy.visit("/?lang=de", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "en-EN" }) + }, + }) + cy.findByRole("link", { + name: "Test Vokabular", + }).should("exist") + }) + + it("The navigator language is used as fallback language, when the language from url param 'lang' is not found", () => { + cy.visit("/?lang=bla", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "en-EN" }) + }, + }) + cy.findByRole("link", { + name: "Test Vocabulary", + }).should("exist") + }) + + it("A fallback language is used, when neither navigator language nor language from url param 'lang' is found", () => { + cy.visit("/?lang=bla", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "fr-FR" }) + }, + }) + cy.findByRole("link", { + name: "Test Vokabular", + }).should("exist") + }) }) diff --git a/gatsby-browser.js b/gatsby-browser.js index 3b2cdab3..61384741 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -12,5 +12,9 @@ export const wrapRootElement = ({ element }) => ( {element} ) -export const wrapPageElement = ({ element, props }) => - props.pageContext.node ? {element} : element +// if the pageContext contains node data, e.g. it's a concept scheme, +// concept or collection it gets wrapped in the App component +// otherwise the present page is delivered (in our case the index page) +export const wrapPageElement = ({ element, props }) => { + return props.pageContext.node ? {element} : element +} diff --git a/src/common.js b/src/common.js index 7d04f2fd..54c0efd7 100644 --- a/src/common.js +++ b/src/common.js @@ -194,6 +194,26 @@ function loadConfig(configFile, defaultFile) { return config } +/** + * Location object from reach router, see https://www.gatsbyjs.com/docs/location-data-from-props/ + * @typedef {Object} Location + * @property {string} key + * @property {string} pathname + * @property {string} search + */ + +/** + * Parses the location object for an URL parameter "lang". + * If multiple "lang" params are given, the first one is taken. + * @param {Location} location + * @returns {string|null} parsed language or null if none is given + */ +const getLanguageFromUrl = (location) => { + const params = new URLSearchParams(location.search) + const language = params.get("lang") + return language +} + module.exports = { i18n, getFilePath, @@ -203,4 +223,5 @@ module.exports = { getLinkPath, parseLanguages, loadConfig, + getLanguageFromUrl, } diff --git a/src/components/Collection.jsx b/src/components/Collection.jsx index a7a13248..4dcd86b9 100644 --- a/src/components/Collection.jsx +++ b/src/components/Collection.jsx @@ -3,36 +3,12 @@ import { i18n, getFilePath } from "../common" import JsonLink from "./JsonLink" import { useSkoHubContext } from "../context/Context" import { useEffect, useState } from "react" -import { replaceFilePathInUrl } from "../common" -import { useLocation } from "@gatsbyjs/reach-router" -import { getConfigAndConceptSchemes } from "../hooks/configAndConceptSchemes" const Collection = ({ pageContext: { node: collection, customDomain } }) => { - const { data, updateState } = useSkoHubContext() + const { data } = useSkoHubContext() const [language, setLanguage] = useState("") - const pathName = useLocation().pathname.slice(0, -5) - const { config } = getConfigAndConceptSchemes() useEffect(() => { - ;(async function () { - for await (const member of collection.member) { - const path = replaceFilePathInUrl( - pathName, - member.id, - "json", - config.customDomain - ) - const res = await (await fetch(path)).json() - const cs = res.inScheme[0] - if (res.type === "Concept") { - updateState({ - ...data, - currentScheme: cs, - }) - break - } - } - })() if (data.selectedLanguage !== "") { setLanguage(data.selectedLanguage) } diff --git a/src/components/Concept.jsx b/src/components/Concept.jsx index 8222ef58..4d3aaf99 100644 --- a/src/components/Concept.jsx +++ b/src/components/Concept.jsx @@ -11,29 +11,13 @@ const Concept = ({ pageContext: { node: concept, collections, customDomain }, }) => { const conceptSchemes = getConceptSchemes() - const { data, updateState } = useSkoHubContext() + const { data } = useSkoHubContext() const [language, setLanguage] = useState("") useEffect(() => { setLanguage(data.selectedLanguage) }, [data?.selectedLanguage]) - useEffect(() => { - if (!Object.keys(data.currentScheme).length) { - updateState({ - ...data, - currentScheme: concept.inScheme[0], - indexPage: false, - }) - } - if (data.indexPage) { - updateState({ - ...data, - indexPage: false, - }) - } - }, []) - return (

diff --git a/src/components/ConceptScheme.jsx b/src/components/ConceptScheme.jsx index ceaee04b..801f4d3c 100644 --- a/src/components/ConceptScheme.jsx +++ b/src/components/ConceptScheme.jsx @@ -10,28 +10,12 @@ import { useLocation } from "@gatsbyjs/reach-router" const ConceptScheme = ({ pageContext: { node: conceptScheme, embed, customDomain }, }) => { - const { data, updateState } = useSkoHubContext() + const { data } = useSkoHubContext() const [language, setLanguage] = useState("") useEffect(() => { setLanguage(data.selectedLanguage) }, [data?.selectedLanguage]) - useEffect(() => { - if (!Object.keys(data.currentScheme).length) { - updateState({ - ...data, - currentScheme: conceptScheme, - indexPage: false, - }) - } - if (data.indexPage) { - updateState({ - ...data, - indexPage: false, - }) - } - }, []) - const pathname = useLocation() // got some hash uri to show diff --git a/src/components/header.jsx b/src/components/header.jsx index d2aade37..9f84a729 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -109,12 +109,12 @@ const Header = ({ siteTitle }) => { availableLanguages: languages, }) setLanguage(userLang) - updateState({ ...data, selectedLanguage: userLang }) + // updateState({ ...data, selectedLanguage: userLang }) } else { setLanguage(data.selectedLanguage) } } - }, [languages]) + }, [data]) // Set Languages useEffect(() => { @@ -123,15 +123,7 @@ const Header = ({ siteTitle }) => { } else { setLanguages(conceptSchemesData[data.currentScheme.id].languages) } - }, [data?.currentScheme?.id, data?.languages]) - - // we check if we are on the root i.e. index page. - // if so we set the concept scheme to an empty object - useEffect(() => { - if (data.indexPage === true) { - updateState({ ...data, currentScheme: {} }) - } - }, [data?.indexPage]) + }, [data]) return (
@@ -166,7 +158,7 @@ const Header = ({ siteTitle }) => { config.customDomain )} > - {data.currentScheme?.title?.[language] || + {data.currentScheme?.title?.[data.selectedLanguage] || data.currentScheme.id}

diff --git a/src/hooks/getUserLanguage.js b/src/hooks/getUserLanguage.js index e5ef0bfd..5c7c2760 100644 --- a/src/hooks/getUserLanguage.js +++ b/src/hooks/getUserLanguage.js @@ -6,12 +6,15 @@ export const getUserLang = ({ availableLanguages = [], selectedLanguage }) => { if (typeof window !== "undefined") { /** @prop {string} */ const userLang = (navigator.language || navigator.userLanguage).slice(0, 2) - if (selectedLanguage && availableLanguages.includes(selectedLanguage)) + if (selectedLanguage && availableLanguages.includes(selectedLanguage)) { return selectedLanguage - else if (selectedLanguage && !availableLanguages.includes(selectedLanguage)) - return availableLanguages[0] - else if (selectedLanguage) return selectedLanguage - else if (availableLanguages.includes(userLang)) return userLang + } else if ( + selectedLanguage && + !availableLanguages.includes(selectedLanguage) && + availableLanguages.includes(userLang) + ) { + return userLang + } else if (availableLanguages.includes(userLang)) return userLang else { const language = availableLanguages[0] return language diff --git a/src/pages/index.js b/src/pages/index.js index 6e777a45..f659a6ba 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react" import { Link } from "gatsby" -import { i18n, getFilePath } from "../common" +import { i18n, getFilePath, getLanguageFromUrl } from "../common" import { useSkoHubContext } from "../context/Context" import { getUserLang } from "../hooks/getUserLanguage" import { getConfigAndConceptSchemes } from "../hooks/configAndConceptSchemes.js" @@ -8,7 +8,7 @@ import { getConfigAndConceptSchemes } from "../hooks/configAndConceptSchemes.js" import Layout from "../components/layout" import SEO from "../components/seo" -const IndexPage = () => { +const IndexPage = ({ location }) => { const [conceptSchemes, setConceptSchemes] = useState([]) const [language, setLanguage] = useState("") const { data, updateState } = useSkoHubContext() @@ -30,13 +30,33 @@ const IndexPage = () => { // set language stuff useEffect(() => { - if (data?.languages) - setLanguage( - getUserLang({ - availableLanguages: data.languages, - selectedLanguage: data?.selectedLanguage || null, - }) - ) + const languageFromUrl = getLanguageFromUrl(location) + if (languageFromUrl && !data.selectedLanguage) { + const userLang = getUserLang({ + availableLanguages: data.languages, + selectedLanguage: languageFromUrl, + }) + + setLanguage(userLang) + updateState({ + ...data, + selectedLanguage: userLang, + indexPage: true, + currentScheme: {}, + }) + } else { + const userLang = getUserLang({ + availableLanguages: data.languages, + selectedLanguage: data?.selectedLanguage || null, + }) + setLanguage(userLang) + updateState({ + ...data, + selectedLanguage: userLang, + indexPage: true, + currentScheme: {}, + }) + } }, [data?.languages, data?.selectedLanguage]) return ( diff --git a/src/templates/App.jsx b/src/templates/App.jsx index 3f70895e..6ec85a73 100644 --- a/src/templates/App.jsx +++ b/src/templates/App.jsx @@ -1,6 +1,11 @@ import React, { useEffect, useState } from "react" import escapeRegExp from "lodash.escaperegexp" -import { i18n, getFilePath } from "../common" +import { + i18n, + getFilePath, + getLanguageFromUrl, + replaceFilePathInUrl, +} from "../common" import NestedList from "../components/nestedList" import TreeControls from "../components/TreeControls" import Layout from "../components/layout" @@ -9,13 +14,14 @@ import Search from "../components/Search" import { conceptStyle } from "../styles/concepts.css.js" import { getConfigAndConceptSchemes } from "../hooks/configAndConceptSchemes" +import { getUserLang } from "../hooks/getUserLanguage" import { useSkoHubContext } from "../context/Context.jsx" import { withPrefix } from "gatsby" import { handleKeypresses, importIndex } from "./helpers" -const App = ({ pageContext, children }) => { - const { data, _ } = useSkoHubContext() - const { config } = getConfigAndConceptSchemes() +const App = ({ pageContext, children, location }) => { + const { data, updateState } = useSkoHubContext() + const { config, conceptSchemes } = getConfigAndConceptSchemes() const style = conceptStyle(config.colors) const [index, setIndex] = useState({}) const [query, setQuery] = useState(null) @@ -45,9 +51,77 @@ const App = ({ pageContext, children }) => { } const [language, setLanguage] = useState("") + const [currentScheme, setCurrentScheme] = useState(null) + + // get current scheme + useEffect(() => { + const fetchConceptSchemeForCollection = async (collection) => { + for (const member of collection.member) { + const path = replaceFilePathInUrl( + location.pathname, + member.id, + "json", + config.customDomain + ) + const res = await (await fetch(path)).json() + const cs = res.inScheme[0] + if (res.type === "Concept") { + return cs + } + } + } + + const getCurrentScheme = async () => { + if (pageContext.node.type === "ConceptScheme") + setCurrentScheme(pageContext.node) + else if (pageContext.node.type === "Concept") + setCurrentScheme(pageContext.node.inScheme[0]) + else if (pageContext.node.type === "Collection") { + const cs = await fetchConceptSchemeForCollection(pageContext.node) + setCurrentScheme(cs) + } else return {} + } + getCurrentScheme() + }, []) + + // set language stuff useEffect(() => { - data?.conceptSchemeLanguages && setLanguage(data.selectedLanguage) - }, [data?.selectedLanguage]) + if (currentScheme) { + const languageFromUrl = getLanguageFromUrl(location) + + if (languageFromUrl && !data.selectedLanguage) { + const userLang = getUserLang({ + availableLanguages: data?.conceptSchemeLanguages, + selectedLanguage: languageFromUrl, + }) + setLanguage(userLang) + updateState({ + ...data, + currentScheme, + indexPage: false, + selectedLanguage: userLang, + availableLanguages: conceptSchemes[currentScheme.id].languages, + }) + } else { + const userLang = getUserLang({ + availableLanguages: conceptSchemes[currentScheme.id].languages, + selectedLanguage: data?.selectedLanguage || null, + }) + setLanguage(userLang) + updateState({ + ...data, + currentScheme, + indexPage: false, + selectedLanguage: userLang, + availableLanguages: conceptSchemes[currentScheme.id].languages, + }) + } + } + }, [data?.languages, data?.selectedLanguage, currentScheme]) + + // useEffect(() => { + // data?.conceptSchemeLanguages && setLanguage(data.selectedLanguage) + // }, [data?.selectedLanguage]) // Fetch and load the serialized index useEffect(() => { diff --git a/test/App.test.jsx b/test/App.test.jsx index 4d3a4ad7..d640b4f9 100644 --- a/test/App.test.jsx +++ b/test/App.test.jsx @@ -30,11 +30,15 @@ vi.mock("flexsearch/dist/module/document.js", async () => { }) const useStaticQuery = vi.spyOn(Gatsby, `useStaticQuery`) -function renderApp(history, pageContext, children = null) { +function renderApp(history, pageContext, location, children = null) { return render( - + ) @@ -65,8 +69,9 @@ describe("App", () => { it("renders App component with expand and collapse button", async () => { const route = "/w3id.org/index.html" const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } await act(() => { - renderApp(history, ConceptSchemePC) + renderApp(history, ConceptSchemePC, location) }) expect(screen.getByRole("button", { name: "Collapse" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument() @@ -75,6 +80,7 @@ describe("App", () => { it("renders App component **without** collapse and expand button", async () => { const route = "/w3id.org/index.html" const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } // remove narrower from concept const topConcept = ConceptSchemePC.node.hasTopConcept[0] @@ -87,7 +93,7 @@ describe("App", () => { } await act(() => { - renderApp(history, pageContext) + renderApp(history, pageContext, location) }) expect(screen.queryByRole("button", { name: "Collapse" })).toBeNull() expect(screen.queryByRole("button", { name: "Expand" })).toBeNull() @@ -97,9 +103,10 @@ describe("App", () => { window.HTMLElement.prototype.scrollIntoView = function () {} const route = "/w3id.org/c1.html" const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } await act(() => { - renderApp(history, ConceptPC) + renderApp(history, ConceptPC, location) }) // we render the concept with notation therefore the "1" expect( @@ -112,9 +119,10 @@ describe("App", () => { window.HTMLElement.prototype.scrollIntoView = function () {} const route = "/w3id.org/collection.html" const history = createHistory(createMemorySource(route)) + const location = { search: "", pathname: route } await act(() => { - renderApp(history, CollectionPC) + renderApp(history, CollectionPC, location) }) // we render the concept with notation therefore the "1" expect( @@ -128,8 +136,10 @@ describe("App", () => { const user = userEvent.setup() const route = "/w3id.org/index.html" const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } + await act(() => { - renderApp(history, ConceptSchemePC) + renderApp(history, ConceptSchemePC, location) }) expect(screen.queryByText("Konzept 1")).toBeInTheDocument() expect(screen.queryByText("Konzept 2")).toBeInTheDocument() diff --git a/test/common.test.js b/test/common.test.js index f1d7a19c..01ec4926 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -5,6 +5,7 @@ const { getFilePath, replaceFilePathInUrl, getLinkPath, + getLanguageFromUrl, } = require("../src/common") describe("Translate", () => { @@ -75,3 +76,18 @@ describe("getLinkPath", () => { ).toBe("../1.de.html") }) }) + +describe("getLanguageFromUrl", () => { + it("parses language if lang param in location.search is given", () => { + const location = { + search: "?lang=de", + } + expect(getLanguageFromUrl(location)).toBe("de") + }) + it("returns null, if no lang param is present in location.search", () => { + const location = { + search: "", + } + expect(getLanguageFromUrl(location)).toBeNull() + }) +})